Commit Graph
2494 Commits
Author SHA1 Message Date
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 b9a98cef8f feat: rebuild the unified inbox as three columns with no metric strip, a flattened scope rail with one row language and bare counts, three-line conversation rows carrying an unread dot in the gutter instead of an avatar and bar, a subject-first thread header with icon-only actions, a filter popover that applies on the spot and shows each added filter as a removable chip that never repeats what the current view already fixes, and real loading throughout: a delayed progress bar over dimmed stale rows, row-shaped skeletons for first load, next page and the thread reader, and optimistic row removal so archive, delete and snooze land instantly 2026-09-14 08:26:03 -07:00
Matthew Meszaros a52a894110 feat: let the OpenAI provider adapt to a model that refuses function tools unless reasoning is off, by flipping a sticky reasoning_effort=none flag on the 400 that names it and widening the per-call compatibility retry budget to cover every flag, since gpt-5.6-luna rejects three parameters in a row and the old budget of two ended the call before the third adaptation (#513) v0.4.12 2026-09-14 08:20:08 -07:00
Matthew Meszaros 5ec367de8a fix: put the mailbox signature and the opt-out footer inside the container an HTML email was laid out in instead of after it, by locating that container with a new offset-keeping outline scan in internal/pkg/mailhtml and splicing into it, and centring the line on the card's own width when a builder export has no single container to sit in, so neither renders hard left in the page background any more (issue #462) (#505) 2026-09-14 08:11:52 -07:00
Matthew Meszaros c28f915648 feat: erase everything a disconnected mailbox leaves behind, revoking its OAuth grant at Google and deleting its stored message bodies through a durable retried queue, cascade the nine mailbox foreign keys that had none so warmup receipts, tampering events and provider message maps stop outliving the mailbox, clear thread labels and snoozes on conversations the delete emptied, make workspace deletion possible at all by cascading the four organization foreign keys with no delete action, and put Disconnect in the mailbox row menu and a Settings danger zone since it was only reachable from the selection bar (#506) 2026-09-14 07:55:01 -07:00
Matthew Meszaros d74d5e6836 fix: retire the warmup spam score, a counter that grew with volume rather than misbehaviour and that no band could act on (#508)
* fix: retire the warmup spam score, a ratchet that grew with volume rather than misbehaviour and that no band ever read, dropping the column from the pool row and the reputation ledger and explaining a pool finding with the band's own reason instead (#491)

* test: pin the advisor snapshot's pool columns against the scan, since the band's reason now reaches the finding through that select alone (#491)

* fix: hold a warmup sentence's score and reason with the sentence itself, keep the retired spam_score key on the published analytics payload as a deprecated zero, seed the sandbox with severity-shaped scores, and record the raw spam report when the warmup service is absent (#491)
2026-09-14 07:44:34 -07:00
Matthew Meszaros 6fafb8bd6a fix: honour a warmup routing rule of weight 0 as an exclusion, dropping the pair before the draw and refusing it on the reply-back, so a pool of one can no longer smuggle an excluded partner past a weighting (#501) (#504) 2026-09-14 03:36:04 -07:00
Matthew Meszaros 2f6c027e37 fix: take the warmup health sweep from twelve round-trips per mailbox to two, list participants stalest first, stop at the deadline, and return the standing from the write (#502)
* perf: cut the warmup health sweep from twelve round-trips per mailbox to seven by reading the participant row once, counting placements and complaints in one scan, complaints and bounces in one scan, and taking the spam score from the row already in hand (#492)

* perf: take the warmup health sweep to two round-trips per mailbox (one metrics statement, the write returns the row), list participants stalest first and stop at the deadline, and drop the dead spam-score and count surface (#492)
2026-09-14 03:14:15 -07:00
Matthew Meszaros 2bbd72e758 fix: make cross-tier warmup borrowing real and one-directional: a thin premium tier borrows proven free mailboxes through one repository rule, gates each drawn partner in its own pool, and only reply-backs cross tiers (#496)
* fix: gate a warmup partner borrowed from the other tier against the pool it is in rather than the sender's, since the thin-tier fallback had rejected every borrowed candidate and a thin tier failed instead of borrowing

* fix: make cross-tier warmup borrowing one-directional and gate borrowed partners in their own pool, so a thin premium tier can actually borrow proven free mailboxes (#495)

* fix: pin the borrow floor at the exact boundary so a premium tier at the floor including its sender still borrows (#495)

* fix: put the warmup borrowing rule in one repository method (direction, floor, proven age, workspace standing) that the selector and scheduler both read, pin every drawn partner's gate to its own pool, fall through buckets when a stale row fails the gate, and allow only reply-backs across tiers (#495)

* fix: end the warmup partner draw by candidate exhaustion instead of a fixed attempt cap, and fail closed when a free mailbox's workspace standing cannot be read before it answers into a paid inbox (#495)

* fix: end the warmup partner draw by candidate exhaustion instead of a fixed attempt cap, and fail closed when a free mailbox's workspace standing cannot be read before it answers into a paid inbox (#495)
2026-09-14 02:51:02 -07:00
Matthew Meszaros 40506c4f05 fix: seed the two warmup pools on every instance under fixed ids and make one pool per type structural, since the baseline squash dropped the insert and a fresh self-hosted instance never warmed; move memberships onto the canonical pools, scope the standing mirror trigger to the columns it mirrors so a pool move keeps a retention window, commit the runtime and every seeder to the ids through MoveToPool, assert the pools at boot and in a warmup_pools_missing health check, and drop the guide's claim of cross-tier borrowing the health gate rejects (#493) 2026-09-13 21:37:17 -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
v0.4.11
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 1dc4aedc3c fix: retire the warmup invalid-token band with its table, metric query, service and repository methods and admin tab, since nothing has fed it since #481 and no attributable forged-token signal exists; key the live pool fixtures on the canonical pool ids so the warmup, repository routing and tasks routing suites run on a fresh database, and correct every doc, site and advisor line that still described the retired signal or a spam-score threshold nothing implements (#490) 2026-09-13 08:09:01 -07:00
Matthew Meszaros e49a7c4c3a feat: count only dispatched sends against a mailbox's sending profile, so deferral wake-ups and the campaign chain's next queued run stop spending the rolled daily plan (issue #469) (#475) 2026-09-13 06:47:42 -07:00
Matthew Meszaros 1c55b93afb Every shortcut the ? modal shows now runs, and both resize handles share one gesture (#487)
* fix: make the shortcuts modal and the key dispatcher one registry so a row that runs nothing cannot be written, wire j/k/gg/G/Enter/Escape and / to the screen that owns them instead of to store fields nobody wrote, unshadow g k, and give the assistant panel's resize handle the pointer capture, single store write, bounds and separator keyboard the unibox splitter already had

* fix: lock text selection for the assistant window's move and corner drags instead of cancelling their pointerdown, which took the compatibility mousedown with it and left every open popover on screen

* fix: keep a half-typed g sequence from swallowing a modifier combo, so g followed by Ctrl+K opens the command palette instead of navigating, and end the sequence when any other shortcut fires

* fix: word the inbox keyboard docs so they hold whichever way the conversation list is sorted

* fix: list the command palette combo at the end of the actions group in the shortcuts modal, where a modifier combo reads as a footnote rather than the first thing a bare-key list shows

* fix: release the page-wide selection lock from a window listener as well, so a floating-window drag interrupted by the panel unmounting cannot leave the whole app stuck at user-select none

* fix: drop the unused test-seam export from the shortcut action registry, which is the same unreferenced-helper shape this branch is deleting everywhere else
2026-09-13 05:28:34 -07:00
Matthew Meszaros 9074f2a9cb feat: host the Microsoft identity association file at site/public/.well-known/microsoft-identity-association.json so warmbly.com verifies as the publisher domain for the Entra app registration that connects Outlook mailboxes (#486) 2026-09-13 04:54:49 -07:00
Matthew Meszaros 8799680166 fix: never charge a mailbox for a warmup token that arrived in its inbox, hold a quarantine or block for its full term against fresh metrics, and keep a penalised address's standing across removal, pool exit and export through a trigger-maintained mirror, since the recipient never controlled the token, the bands read seven days against 30-day terms, and the pool row died on paths a snapshot at deletion never saw (#481) 2026-09-13 04:34:44 -07:00
Matthew Meszaros 43dcbde06c feat: tester accounts, creatable from the admin panel (#483)
* feat: excuse one named account from the emailed login code, so a vendor reviewer who cannot read this instance's mail can sign in without turning codes off for everyone, with the reason recorded beside it and every run of warmblyctl status naming the accounts that hold one

* feat: create and manage tester accounts from the admin panel, so letting a reviewer in is a form rather than a shell, with the password shown once and every live exemption listed on one page because forgetting one is the way this goes wrong

* fix: give tester management its own permission bit rather than borrowing ban_users, create the account and its exemption in one transaction so no invisible orphan survives a failure, require an accountable operator on the CLI grant, stop a halted row scan reading as the whole exempt list, and show a failed query as an error instead of as no testers

* chore: re-run CI after the aggregator tripped on a cancelled job from the branch update, with every underlying job green

* chore: retrigger CI, the previous run sat queued indefinitely while other branches ran

* feat: roll back a half-created tester when its workspace step fails and backfill the manage-testers bit onto admins already holding every other permission, so the address is not left taken by an unusable account and the new routes are not 403 for the existing admin

* feat: make the 000151 manage-testers backfill one-way, because clearing bit 22 on the way down would also revoke it from an admin granted it explicitly afterwards and the up migration would not restore that
v0.4.10
2026-09-13 03:07:10 -07:00
Matthew Meszaros 55d712579b feat: collapse the left navigation to an icon rail, drag the unibox conversation list against the thread, and remember the contact rail toggle instead of reopening it on every conversation (#479) 2026-09-13 01:09:29 -07:00
Matthew Meszaros 2edccf812c fix: carry the managed flag on the base subscription type so GET /subscription reports it, because that is the endpoint the dashboard decides entitlements from and the flag only existed on the limits response (#478) v0.4.9 2026-09-12 22:32:14 -07:00
joaoppa fe20326815 fix: stop the warmup tampering detector charging a mailbox for re-reading its own mail, since its Sent copy carries the recipient's token and a re-synced or reconnected mailbox presents tokens that no longer resolve, while keeping a token that names another pair as signal in any folder at any age (#468) 2026-09-12 22:20:42 -07:00
Matthew Meszaros 7f74664c72 fix: a granted plan unlocks nothing, and Turnstile never renders (#474)
* fix: let a granted plan unlock the dashboard, since the client decided paid from the Stripe status a managed subscription never touches, and replace the Turnstile size Cloudflare removed so the widget renders and can issue a token at all

* feat: tell people on a preview deployment that it is a public beta, once in a dialog and thereafter as a header pill they can reopen, driven by a config value rather than a hostname so one image stays reusable, and bind both Turnstile modals through onLoad because the component is not forwardRef and execution=execute never fires without the widget instance

* fix: keep the beta pill outside the desktop-only header group so the notice stays reopenable on a phone, and say in the docs that the value is baked into config.js at container start rather than read per load
v0.4.8
2026-09-12 21:45:05 -07:00
Matthew Meszaros 017f4cf60f feat: plans an operator can grant, visible in the admin panel (#467)
* feat: add operator-granted plans so a workspace can be paid without Stripe, surfaced in the admin panel as a badge, a filter and a card carrying who granted it and why, because the only alternative was writing a fake stripe subscription id into the database

* fix: hold a granted plan beside the paid one rather than over it so a Stripe workspace returns to the plan it pays for when the grant ends, route entitlement lookups through EffectivePlanID, separate a repository failure from an unknown plan, end a grant at local end of day, and drop an index that served no query
v0.4.7
2026-09-12 09:45:31 -07:00
Matthew Meszaros de38a10748 feat: AI is a paid feature, so take it off the free plan (#466)
* feat: take AI off the free plan, zeroing the free allowance and gating the writing assistant to paid like the inbox agent already was, because a trial that can only ever be told it has no credits reads as a broken feature rather than a locked one

* feat: state in the down migration which case does not round-trip, a free plan already at zero before 000148 ran, since the up migration recorded no prior value and a zero it did not write cannot be told from one it did
2026-09-12 09:09:00 -07:00
Matthew Meszaros d8d929c3f0 fix: stop the contacts and campaign-leads table overflowing its panel on a long company name (#461)
Under auto table layout one long company name set its column's min-content and widened the table past the content panel, putting a horizontal scrollbar under the whole list; `truncate` on the `<td>` gave it nowrap with no width constraint and never rendered an ellipsis. Moves the table to 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 panel at any width.

Also closes the pre-existing overflow at md/lg, where the Leads view's sized columns alone exceeded the panel regardless of content, and gives both status pills an accessible name below sm where the label had been display:none.

Closes #461.
2026-09-12 08:33:15 -07:00
Matthew Meszaros 1f7e036659 fix: mailbox onboarding accepts connections that cannot work, and hides why (#465)
* fix: report why a provider refused to name a mailbox during onboarding, since the status and body were read and then discarded and five distinct failures arrived as one opaque sentence with nothing in the logs or error tracking behind it

* fix: refuse a mailbox the provider only partly authorised, since a consent screen lets the person untick individual permissions and still issues a token, and refuse a first connect that returns no refresh token because that mailbox has an hour to live

* fix: never record a provider's successful profile payload in diagnostics because a decode failure carries status 200 and that body is the mailbox owner's address, and bound the provider read so a broken intermediary cannot make onboarding read arbitrary memory
v0.4.6
2026-09-12 08:07:20 -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 06b8db5529 feat: report the one silent state a campaign had no words for, a mailbox pool that is part out of budget and part outside its own sending hours, which fell between the 'every mailbox is capped, sending resumes tomorrow' line and the deliberately unlogged closed-hours band and so left an active campaign sending nothing with an empty activity feed; give it its own line under the mailboxes_unavailable event the pool's other refusals already use, naming how many mailboxes are in each state and carrying the moment the pool comes back in metadata rather than promising a day, while leaving daily_cap_reached to mean exactly what it meant before, every usable mailbox spent and nothing coming back until tomorrow 2026-09-12 03:47:36 -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 5869a2148f feat: stop a campaign sending outside its sending window, and past its end date, when a follow-up is overdue: the placer's schedule gates are asked about the moment a step became due, and nextScheduleSlot deliberately returns an instant that was already inside a window unchanged, so a step that came due at 2pm still read as due at 11pm and the task sent it there, while the end-date comparison found a candidate predating the end date; floor an overdue candidate at now so the window, the weekday mask and the end date are all asked about the send that is actually about to happen, with live tests for a closed window, a passed end date and the overdue step that must still go out promptly while the window is open 2026-09-12 03:29:33 -07:00
Matthew Meszaros 7754dac629 Merge pull request #458 from warmbly/feature/contact-page-real-data
feat: rebuild the marketing contact page with real channels only
2026-09-12 03:15:04 -07:00
Matthew Meszaros 5857a6ddd8 Merge remote-tracking branch 'origin/main' into feature/contact-page-real-data 2026-09-12 03:13:42 -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 9ce576bb3f feat: rebuild the marketing contact page as a minimal list of real channels (team@warmbly.com, the Discord, GitHub issues and discussions, docs, status) and the Mindroot Ltd registration, dropping the invented Delaware entity, postal address, team locations, business hours and SLA 2026-09-12 03:02:57 -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 b0050507e4 fix: tell a joining node a tag that exists, falling back to the published prod tag instead of a latest this project has never published, and drop the WORKER_IMAGE docs left behind by the removed push-based provisioning (#455) v0.4.5 2026-09-12 01:00:11 -07:00
Matthew Meszaros 26f4549f63 fix: redact the credential from the Redis URL the realtime service logs on every boot, keeping the address that makes the line useful (#454) 2026-09-12 00:52:18 -07:00
Matthew Meszaros ca1c10ec72 fix: give the realtime Redis event bridge the same wildcard TLS options as the command pool, because it is a second connection that a managed Redis rejects and it is the one that delivers every realtime event (#453) 2026-09-12 00:41:30 -07:00
Matthew Meszaros a4f503f620 fix: realtime cannot complete either of its TLS connections (#452)
* fix: let the realtime service actually establish its TLS connections, adding the wildcard hostname match fun Redix needs for every managed Redis and a CA bundle option for an RDS Postgres that the system trust store cannot verify

* feat: drop the redundant wget package from the realtime image since busybox already provides it, and fix a comment that read as literal interpolation

* fix: treat a blank DATABASE_SSL_CA_FILE as unset in the realtime config, because compose passes every optional variable through as empty and cacertfile with an empty path is no CA source at all rather than a fallback to the system store
2026-09-12 00:29:34 -07:00
Matthew Meszaros ee9e8706fb feat: build librdkafka with OpenSSL in the tracking service so security.protocol=SASL_SSL works, which every managed broker requires and which the cmake-build-only feature set silently disabled (#451) 2026-09-11 23:37:20 -07:00
Matthew Meszaros fa252aebe8 Merge pull request #443 from warmbly/feature/editor-image-links-and-buttons
feat: clickable images and a call-to-action button in the campaign body editor
2026-09-11 23:06:29 -07:00
Matthew Meszaros 8a2856fce0 Merge branch 'main' into feature/editor-image-links-and-buttons 2026-09-11 23:02:15 -07:00
Matthew Meszaros e8186e5f52 fix: the tracking service must not write Avro to a JSON consumer (#450)
* feat: make the Rust tracking publisher honour CODEC_PROVIDER on Kafka instead of always writing Avro, so a json consumer stops silently dropping every open and click, and refuse avro at boot when no Schema Registry is configured

* feat: build the tracking service's kafka feature in CI, because clippy on the default build never opens kafka.rs and that file now ships as the published tracking -kafka image

* feat: install libcurl and the rest of the librdkafka build dependencies for the tracking kafka clippy step, which fails at the first object without curl headers even with WITH_CURL=0
2026-09-11 23:00:15 -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 184cc951f0 Merge pull request #444 from warmbly/fix/issue-432
fix: make Edit with AI apply the instruction instead of rewriting the body into a fresh cold email
2026-09-11 22:52:54 -07:00
Matthew Meszaros 88b483d227 Merge branch 'main' into fix/issue-432 2026-09-11 22:47:54 -07:00
Matthew Meszaros 7d79dcc282 feat: append a -kafka image variant to every version the control plane hands a fleet node when the instance runs Kafka, so a joining worker pulls a build that can actually reach the bus instead of failing at boot (#449) 2026-09-11 22:46:02 -07:00