Matthew Meszaros
9f4b66c48b
feat: record the one bound the clipped-text tooltip keeps, that text replaced by a realtime update under a resting cursor reads its previous value until the pointer leaves and returns, since closing it would cost a layout read per row per frame on mousemove to buy a sub-second window on a tooltip
2026-09-12 08:30:02 -07:00
Matthew Meszaros
bc6c4a0b7e
feat: settle who owns a cell's title attribute between React and the clipped-text tooltip helper, matching on the exact string the helper last wrote rather than on the element, so a caller that starts supplying a title after the helper has already titled that node keeps it, and giving a lead's status label the failure reason as a React-managed title so a transition into failed while the pointer rests on the pill overwrites the status word the helper left there instead of stranding it over the reason for the life of the node
2026-09-12 08:30:02 -07:00
Matthew Meszaros
79758e38fa
feat: stop the clipped-text tooltip helper removing a title it did not set, which would have cost the next caller a React-managed tooltip for good after one hover since React rewrites that attribute only when the prop changes, tracking the elements it titled in a WeakSet instead; and settle the contact row's tag area on one chip plus a count, because two tags sharing a now fixed-width Name column with the name left each of them about three legible characters, while dropping the per-chip clamp that had capped every compact chip at 72px at every width and put a long tag out of reach of a touch or keyboard user
2026-09-12 08:30:02 -07:00
Matthew Meszaros
0b12102be8
feat: name the clipped-text tooltip helper's file after what it actually exports, clippedTitle, now that the module hands out a spreadable pair of mouse handlers rather than the single titleWhenClipped function it started as
2026-09-12 08:30:02 -07:00
Matthew Meszaros
62e98358c2
feat: keep the contacts list's subscribed state and the campaign lead's progress readable by a screen reader below sm, where both pills are their icon alone and the word was display:none, by holding the label in a permanently screen-reader-only span and hiding the visible copy from the accessibility tree instead, which also keeps the sm+ label's truncation intact where sm:not-sr-only would have reset the overflow and white-space that truncate depends on
2026-09-12 08:30:02 -07:00
Matthew Meszaros
f00dd0fdf6
feat: stop the contacts and campaign-leads table widening past its panel when a long company name arrives, by moving it off auto table layout, where one cell's nowrap text sets its column's min-content and pushes a horizontal scrollbar under the whole list, onto table-fixed with a declared width per column and clipping in every cell, Name the one auto column so it takes all the slack, each free-text column truncating into a native tooltip that only appears where the text was actually cut off, and the column set restaged per breakpoint so the sized columns never sum past the content panel at any width ( #461 )
2026-09-12 08:30:02 -07:00
Matthew Meszaros
d456bc48c6
feat: fix the Warmbly Cloud pool link across both roles ( #262 ): take an enrolled mailbox out of this instance's own warmup pool so local partners stop writing to it and their unverifiable warmup stops landing in the owner's unibox, recognise the cloud's warmup mail whose verify header did not survive delivery through a new warmup-deliveries lookup that ignores consumed_at because instance and cloud read the same mailbox, move the managed-mailbox access token route behind NODE_BROKER_TOKEN so the internet-facing tracking and forms services can no longer mint a live provider token, scope pause and resume to the caller's workspace, keep an enrolled mailbox listed once it goes inactive, release the cloud copy when the local mirror row cannot be written, refuse the one-time handshake when CREDENTIALS_ENCRYPTION_KEY is missing, blank an expired code's plaintext instance token, and stop errx answering 200 for a status outside its table
2026-09-12 06:58:25 -07:00
Matthew Meszaros
c4aece241b
feat: scope the tag, category and folder registries and unibox conversation labels to the organization instead of the creating user, so a teammate sees and can edit the labels the owner made, splitting a label two workspaces shared into one copy each and guarding every label write against ids from another workspace ( #457 )
2026-09-12 03:37:31 -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
dc9ce403de
feat: stop one un-sendable lead parking a whole campaign and stop the contact drawer's next-action time walking forward on every refresh (issue #437 ): route up to config.CampaignPlacementCandidates due leads per pass instead of one, classify a placement refusal that belongs to a single lead (ESP-strict finding no mailbox for that recipient's provider, a bound lead inside its own mailbox's minimum gap or waiting for it to reopen, a recipient's send-time-optimized hours) as the new ErrLeadDeferred so the pass moves to the lead behind them and only defers the campaign when every candidate is refused, log the ESP-strict deferral once a day rather than once per refused lead per tick, and make PreviewContactSend a pure read that answers unchanged state identically on every call by running placement with the even-distribution, jitter, conflict-resolution, distribution-curve and sub-minute layers off, taking a behaviour profile's gap at its floor instead of drawing it, picking the mailbox deterministically instead of re-rolling rotation, reporting the next sending day's first open minute instead of a jittered twenty-four-hours-from-now, and reporting a due step's time as the campaign chain's own stored wakeup
2026-09-12 02:58:48 -07:00
Matthew Meszaros
ea8d15374e
Merge branch 'main' into feature/editor-image-links-and-buttons
2026-09-11 22:56:16 -07:00
Matthew Meszaros
1f1112fe93
feat: drop the AI edit's parked review selection when the popover closes, because a result that changes nothing commits no new value and so leaves the layout effect nothing to run on, and the range would then be worn by whatever unrelated edit commits next and yank the caret back into a rewrite the user had already dismissed
2026-09-11 22:42:50 -07:00
Matthew Meszaros
f41eac289d
Merge branch 'main' into fix/issue-432
2026-09-11 22:35:43 -07:00
Matthew Meszaros
4bf412c226
feat: decide the AI edit's No change signal from the value the composer will actually hold rather than from the model's answer, since a rewrite whose every added character falls past the body-length cap leaves the textarea exactly as it was and reporting Rewritten over an unchanged body is the one thing that signal exists to prevent
2026-09-11 22:30:32 -07:00
Matthew Meszaros
ea321ebefb
feat: put the AI edit's review selection back after React commits the value rather than before it, since React writes the textarea's value during commit and that write moves the cursor to the end, leaving the rewrite unselected and Undo's restored range never reaching the DOM, and strengthen the Undo test to assert the range a follow-up run targets instead of the restored value, which Undo sets regardless and which therefore pinned none of the behaviour the commit before it fixed
2026-09-11 22:22:55 -07:00
Matthew Meszaros
af7256ac85
feat: address the third review pass on the Edit with AI fix by recording the range a rewrite replaced instead of working it back out of the lengths afterwards, because the composer's body cap can cut the tail off what was written and the old arithmetic then reconstructed a range that was never selected, so Again re-sent the wrong words and Undo restored the wrong selection, with a test mounting the real textarea host against a small cap to pin both
2026-09-11 21:33:46 -07:00
Matthew Meszaros
069ab15194
feat: address the second review pass on the Edit with AI fix by reading a markdown destination with balanced parentheses so a link the model normalises out of the angle-bracket form it was given comes back whole instead of truncated at the first paren with a stray one left in the copy, restoring only spaces and tabs around the model's answer so a selection that ran to the start of the next paragraph joins the two the way a paste would rather than gaining a blank paragraph nobody typed, and capping the completion at a flat 4096 tokens, above the 3072 it was and below the smallest completion limit in common use, because a request over a backend's own cap earns a 400 naming max_tokens that openAIProvider.adaptParams latches for the life of the process and degrades every later call
2026-09-11 21:21:40 -07:00
Matthew Meszaros
1bdd1da800
feat: address the CodeRabbit review on the Edit with AI fix by leaving the caret after text written at a collapsed position instead of in front of it (an insertion maps to itself unless the position associates rightwards, so continuing to type went before the insert), splitting the model's blocks one separator at a time so a blank paragraph the author used as spacing survives a rewrite instead of being swallowed by a greedy newline run, carrying a link destination that holds a paren or a space through in markdown's angle-bracket form rather than dropping the link, putting the author's boundary whitespace back on the model's trimmed answer in both hosts so a selection ending on a space does not glue the rewrite to the next word and "did anything change?" compares exactly what was written, and sizing the completion cap from the passage's own rune count so an 8000-rune CJK body is not truncated by a cap chosen for English
2026-09-11 20:23:53 -07:00
Matthew Meszaros
63440e9abb
feat: pick a button's label colour by comparing both contrast ratios instead of testing luminance against a threshold, move the sky, emerald and amber swatches to the shade where a white label clears 4.5:1, and clamp the floating node bar inside every viewport edge rather than only the right one
2026-09-11 20:16:07 -07:00
Matthew Meszaros
6501cb599c
feat: fix the "Edit with AI" rewrite in the campaign body and the unibox composer for issue #432 by running /generation/edit on a new generation.BuildEditRules system prompt through AIProvider.Complete instead of the cold-outreach writer prompt that redefined the model's role, capped it at 80 words and imposed a five-part email skeleton on every instruction, raising the completion cap so a full-body rewrite is no longer truncated at 1024 tokens and counting the request limits in runes rather than bytes, carrying merge variables, AI blocks, conditionals, form links and link destinations through the round trip in web/src/components/app/ai/richTextPassage.ts instead of deleting every atom node via doc.textBetween, replacing the passage with paste semantics so a phrase rewritten inside a sentence stops splitting its paragraph into three, saying "No change" when the model hands the passage back untouched, and clamping the floating AI card to the surface it is editing so it no longer draws outside the step drawer over the flow canvas
2026-09-11 20:04:32 -07:00
Matthew Meszaros
3ee636e2e6
Merge branch 'main' into feature/editor-image-links-and-buttons
2026-09-11 20:01:18 -07:00
Matthew Meszaros
7ff9f4d2c6
feat: let a campaign body image carry a link and add a call-to-action button to the editor toolbar, both rendered as mail-safe markup (an anchor-wrapped img, a one-cell table whose cell holds the colour and padding for Outlook's Word engine), with the plain-text half now keeping every link destination
2026-09-11 20:01:16 -07:00
Matthew Meszaros
53d25baa29
feat: read the step performance hover as 'Opens: 1 · 20.0% of 5 sent' instead of the mis-pluralised '1 opens', by labelling each StepMetric cell with its capitalised metric name and putting the count after the colon
2026-09-11 08:57:52 -07:00
Matthew Meszaros
5087023e48
feat: give every campaign step its own open, click, reply and bounce rate in Step performance, computed against that step's own sends, with the automated share of its opens and clicks carried alongside them through GetSequenceStats, the campaign analytics API, the get_campaign_stats AI tool and the docs
2026-09-11 08:56:03 -07:00
Matthew Meszaros
9698052569
Merge branch 'main' into feat/cleanmylist-verification
2026-09-11 06:03:55 -07:00
Matthew Meszaros
27c5df5072
feat: refuse a redirect that downgrades a verification request from https to http in both provider clients, since Go keeps the Authorization header across a redirect that stays on the same host and the pasted API key would go on the wire in the clear, and label the verification action from the connection id rather than the active provider, because a degraded connection reports the built-in checker while still being a connection to manage
2026-09-11 05:26:33 -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
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
4d0f0fb6b6
feat: hold an exhausted verification account that publishes no balance for a cooldown instead of re-deriving its health from an account check that cannot see exhaustion, since CleanMyList answers GET /v1/jobs identically whether or not there is allowance left, so the minute-long lookup cache retired every observed 402 and put the whole next batch back on doomed paid calls while Settings reported the service as healthy, and refuse a second verification connection while one is connected rather than letting creation order silently move every check onto a different bill
2026-09-11 02:57:08 -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
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
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