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
SUMAN JANA
73f6bff2ed
feat: stop a browser signed in as someone else from being offered Accept on an invite it cannot accept, comparing the signed-in address with the invited one on the invite page and offering Switch account instead, and naming both addresses in the backend's 403 so the cause is visible
2026-09-11 03:10:18 -07:00
Matthew Meszaros
170c33780a
feat: pass ui_host to PostHog in the dashboard, admin panel and marketing site from its own environment variable so a proxied api_host stops breaking toolbar and session-replay links, which the SDK builds against whatever it sends events to and which a reverse proxy does not serve, defaulting to us.posthog.com so an install that does not proxy is unaffected
2026-09-11 11:34:02 +02:00
Matthew Meszaros
a84ab48729
Merge branch 'main' into feature/posthog-error-tracking
2026-09-10 10:25:21 -07:00
Matthew Meszaros
84a0ca0b29
feat: correct the noteStep guidance comment to match what the callers actually pass, a bounded route pattern rather than a fixed phrase, so the rule a future caller reads is the rule the trail is built on
2026-09-10 19:16:13 +02: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
d51de7db3a
feat: address the CodeRabbit review by quoting a fragment in the copy's own casing rather than the model's retyping of it, extracting the case-fold offset map into internal/pkg/casefold so the AI half gets the same Unicode safety the rules half has, giving a trigger term a span in each half it appears in instead of losing the second one to deduplication, scanning subject links before body anchors so the display cap cannot drop the subject's own, requiring WRITE_TEMPLATES on the credit-spending analyze route so a read-only key cannot spend the workspace balance, refusing to tell a customer their credits came back when the refund is what failed, and no longer letting a stale analysis retire the newer rules request that was about to replace it
2026-09-10 09:50:28 -07:00
Matthew Meszaros
f468f44e1d
feat: let a content-check issue's field carry the location instead of repeating it in the message, so the launch dialog and campaign feed stop reading 'Body: 3 spam-trigger term(s) found in subject/body', and say 'Subject and body' for an issue whose fragments straddle both halves rather than dropping the location entirely, with the editor labelling each quoted word on such an issue
2026-09-10 09:28:00 -07:00
Matthew Meszaros
ade18d1650
feat: leave an AI finding's field empty when the model labelled neither half and nothing in the finding could be anchored in the copy, instead of defaulting it to the body and rendering a badge that sends the writer to the wrong box on the one panel whose whole purpose is saying which box to open
2026-09-10 09:28:00 -07: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
bbe9d9055a
feat: let web and admin be served from a static host by teaching each app's own entrypoint to render config.js wherever WARMBLY_CONFIG_OUT points, so one definition of the runtime key set serves both the container that renders it at start and a build:pages script that renders it into dist, ship a _redirects in each so a deep link stops 404ing without nginx try_files, and add scripts/check-pages-build.sh to make lint because a malformed config.js reads fine in a diff and leaves the app blank at runtime
2026-09-10 18:03:00 +02:00
Matthew Meszaros
378e1d15ee
feat: build the custom-field record from entries so a contact field named __proto__ is a real key instead of silently setting the prototype and dropping out of what gets saved, and count a row the user has typed a value into but not named as unsaved work, so leaving the panel on it asks rather than discarding it while Save stays disabled because there is nothing to send
2026-09-10 06:54:58 -07:00
Matthew Meszaros
cd051c7150
Merge branch 'main' into fix/issue-415-contact-panel-rebase
2026-09-10 05:42:35 -07:00
Matthew Meszaros
c1a3d52015
feat: address the review of the contact panel rebase, comparing custom-field rows structurally so adopting a server change no longer deletes a row the user has typed a value into but not yet named, comparing records key by key instead of as JSON so re-entering a field in a different order stops leaving the panel permanently unsaved, and comparing id lists sorted so a repeated id cannot pass for a different one
2026-09-10 05:33:26 -07:00
Matthew Meszaros
1e1231f3b1
feat: address the review of the API key delete, putting the permanent-delete route in the published OpenAPI contract with its 409, keeping one auto margin in the drawer footer so the Delete button lands on the right, and deciding the dashboard's status pill, its footer and the key-count strip on whether the key can still authenticate rather than on a status column that never says expired
2026-09-10 05:23:56 -07:00
Matthew Meszaros
653633d8fd
feat: decide what counts as a changed contact field in one place, so the panel's dirty check, its save payload and the new rebase cannot drift apart, and cover the save payload with a test
2026-09-10 05:13:16 -07:00
Matthew Meszaros
7c10ea854c
feat: rebase the contact 360 panel's draft onto the record when it changes underneath, so lifting a suppression on the Overview tab (which re-subscribes the contact) stops reading back as an unsaved edit and asking to discard changes nobody made on close (issue #415 )
2026-09-10 05:07:37 -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
6684dafe3a
feat: classify opens and clicks that arrive from a known mail-filtering network as automated, so Microsoft 365 Defender's delivery-time pixel fetches and Safe Links URL detonations stop counting as engagement (issue #410 ), with a shipped scanner catalogue, TRACKING_SCANNER_* overrides and optional ASN matching from a trusted edge header
2026-09-10 03:38:50 -07:00
Matthew Meszaros
2d20e1a6a0
Merge branch 'main' into fix/unibox-threading-and-transport-errors
2026-09-09 10:08:57 -07:00
Matthew Meszaros
568bdb48ba
feat: never render an empty IMAP error detail and close the CRM rail when the viewport narrows past lg
2026-09-09 09:24:43 -07:00
Matthew Meszaros
3591d64404
Merge remote-tracking branch 'origin/main' into fix/issue-401
2026-09-09 08:58:08 -07:00
Matthew Meszaros
16261de4cb
Merge remote-tracking branch 'origin/main' into fix/issue-401
2026-09-09 08:51:02 -07:00
Matthew Meszaros
68b5d8f358
feat: bind a campaign lead to the mailbox that sends its first email so every follow-up leaves from the same address, holding a lead back while its mailbox is merely out of budget or outside its hours and moving it to another mailbox only when that one can no longer send for the campaign at all
2026-09-09 08:51:00 -07:00
SUMAN JANA
73d9c18bfe
fix(imap, unibox): show the server's text for a codeless IMAP error; contact rail starts closed
...
- An IMAP NO/BAD without a response code (Gmail's "NO System Error")
rendered as "Something went wrong: " in the mailbox's error list. Fall
back to the server's text when the code is empty.
- The unibox contact rail opened by itself on lg+ screens, putting the
contact form in front of every thread the reader opened. It now starts
closed at every width and opens from the header toggle.
2026-09-09 15:47:50 +00:00
Matthew Meszaros
3153f9ff23
Merge remote-tracking branch 'origin/main' into fix/self-hosted-unsubscribe-domain
2026-09-09 08:44:13 -07:00
Matthew Meszaros
cc50f1d026
feat: contain the signature editing surface so a stored inline position:fixed covers its own box instead of a teammate's dashboard, since the visual editor mounts that markup with innerHTML and forcing the source view for every inline style would take the visual editor away from almost every real signature, and stop the editor's table rule overriding a template's own width attribute, which edited a 600px design at full container width and discarded its column widths
2026-09-09 08:33:46 -07:00
Matthew Meszaros
035865e5c5
Merge remote-tracking branch 'origin/main' into feat/issue-393-html-css-email
2026-09-09 08:30:52 -07:00
Matthew Meszaros
f5eeac7b2a
feat: address the CodeRabbit review by carrying every stretch of stylesheet the CSS parser cannot read through as a verbatim item, since the sheet is rewritten from parsed items the moment any rule inlines and an unrelated match was deleting the rest, keeping a link's destination out of the content score now that the text renderer emits it so a CTA pointing at a free-trial page stops costing eight points, promising inlining in the editor only for a stylesheet that is actually eligible for it, skipping the client notes entirely for a plain-text campaign that ships no HTML part, switching a step into HTML mode when a template replaces its body with document markup rather than waiting for the next visual edit to gut it, accepting a pasted background shorthand only when it is a single colour so Word's "yellow none repeat scroll" stops becoming an invalid longhand, listing in SCHEMA_TAGS only the tags the mounted schema actually keeps so the warning fires for h1, font, center, thead and caption instead of staying silent while they are dropped, and correcting the guide's byte-for-byte claim and its unconditional plain-text claim
2026-09-09 08:30:49 -07:00
Matthew Meszaros
6ebf9cfdcf
Merge remote-tracking branch 'origin/main' into fix/self-hosted-unsubscribe-domain
2026-09-09 08:24:04 -07:00
Matthew Meszaros
538dc29575
feat: stop the CSS inliner deleting the half of a selector list it cannot take over, so a rule like ".btn, .other:hover" keeps its declaration for the hover half after the first inlines, leave a rule whose selector list cannot be split on commas exactly as the author wrote it because a comma inside :is() or an attribute value belongs to the selector and rejoining the pieces writes a different one, keep a rule that matches nothing today rather than dropping it, and declare the paste-adoption ref above the editor that closes over it as the other paste-time refs already are
2026-09-09 08:20:23 -07:00
Matthew Meszaros
cdbdad09ee
feat: stop the unibox conversation list resetting to the top when an email is opened, by keying the app shell's route boundary and Suspense on route identity rather than the raw pathname (the open thread is a path segment, so every click was remounting the whole page), remembering each list's scroll offset across remounts and the mobile display:none pane, patching mark-as-read into the query cache instead of refetching every loaded page, and auto-loading the next page at the end of the list (issue #396 )
2026-09-09 06:52:57 -07:00
Matthew Meszaros
7f2bfa252e
feat: hold the campaign preview request until writing settles, since HTML mode runs it from the Edit tab and a designed body is tens of kilobytes to ship on every 250ms pause, and treat a document sitting behind a leading comment as one when deciding whether to adopt a paste as HTML or to open an A/B arm as markup, because a saved email opens with the tool that wrote it before its doctype
2026-09-09 06:39:23 -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
7d58b874b8
feat: keep every recipient-facing and self-host-facing address on the deployment's own domain: mint unsubscribe links on a workspace's verified tracking domain (served by the tracking service, proxied to the backend that owns the pages), attach RFC 8058 one-click only over https, resolve all branding through config.Brand() gated on SelfHosted() so a self-host's email footer, sign-in links, stats card, API example and public form badge name nobody else, drop the app.warmbly.com fallback from AppBaseURL, blank TRACKING_DOMAIN and FORMS_DOMAIN on core-only installs, and have install.sh offer to configure a fresh interactive install instead of silently defaulting to localhost
2026-09-09 06:34:43 -07:00
Matthew Meszaros
1e1bb3622e
feat: address the CodeRabbit review on the email editor PR by generating email image object keys from a uuid and the sniffed extension instead of the uploader's filename so a name carrying '..' cannot produce a key the public route refuses to serve and a private filename never travels inside a recipient's URL, deleting the stored object before its row and refusing the request when storage will not take it so a failed delete leaves nothing loadable that the quota has stopped counting, accepting only https image addresses, resynchronising the HTML source view when a template replaces the body underneath it, and rendering the campaign preview through the inbox's sandboxed frame so markup written in the source view can neither run nor restyle the dashboard for a teammate
2026-09-08 21:17:36 -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
c8042e2942
Merge remote-tracking branch 'origin/main' into feat/campaign-entry-delay
2026-09-08 09:58:38 -07:00
Matthew Meszaros
c35495ae5c
feat: await the trigger card's recovery refetch inside the same promise chain as its saves, so a refused entry-delay commit's invalidation cannot resolve after the next commit's optimistic write and put the stale delay back on the card
2026-09-08 08:34:20 -07:00
Matthew Meszaros
37ed967919
Merge remote-tracking branch 'origin/main' into fix/issue-382-user-experience
2026-09-08 08:32:31 -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
f8c0899be4
feat: address the CodeRabbit review by promoting a rounded wait into the next unit so a contact drawer never reads 60 minutes or 24 hours, describing the entry delay's anchor as the lead's own enrolment moment with the campaign's creation date only as the legacy fallback in both the campaigns and segments guides, replacing the launch dialog's 'sending begins immediately' line when a delay is set, following an outside value change with the picker's preset or custom mode, and chaining the trigger card's saves so two commits inside one round trip cannot land out of order
2026-09-08 07:30:24 -07:00
Matthew Meszaros
78dd032a06
feat: address the CodeRabbit review on the contacts pagination PR by refusing a cursor boundary the SQL cast would choke on so a hand-made token is a 400 instead of a database error surfacing as a 500, writing timestamp boundaries with an explicit to_char pattern the Go validator mirrors so a token never depends on the server DateStyle, basing the full-screen error guard on rows loaded rather than rows left after the client-side subscription filter, keeping the failure visible when that filter hides every loaded row, and pointing Try again at the request that actually failed by reading isFetchNextPageError instead of guessing from hasNextPage
2026-09-08 06:53:57 -07:00
Matthew Meszaros
03b6188364
feat: tighten the import result's failed-pin warning so it contrasts the two outcomes instead of joining them with an and that reads as if both succeeded
2026-09-08 06:36:34 -07:00
Matthew Meszaros
f46728733e
feat: address the CodeRabbit review on the select-all PR by tying the header checkbox to the rows on screen and making it put unticked rows back before it clears a select-all, bounding the exclusion list before it sizes a map, skipping half-filled custom-field pills when a filter is saved as a segment so the segment matches the search, and naming the fifty-thousand selection cap on the campaigns and segments guides
2026-09-08 05:39:16 -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
4e5a6018fc
feat: keep the entry-delay field from re-normalising its own keystrokes, so typing 60 in the minutes box no longer flips to 1 hours under the cursor, and bound the typed amount per unit so switching 45 minutes to days shows the 90 days it is actually clamped to
2026-09-08 05:17:00 -07:00
Matthew Meszaros
8692b1cb52
feat: give the entry-delay picker its own draft state with a separate onCommit callback, so the flow canvas's trigger card saves a typed custom amount on blur, Enter or a stepper click instead of firing one PATCH per keystroke, while the Schedule tab and wizard keep batching every edit into their Save button
2026-09-08 05:11:14 -07:00