v35.0
Notifuse/notifusev35.0Jul 8, 2026by pierre-b
AI Summary
This release introduces per-link click reporting for broadcast reports, significantly improves email rendering performance by reusing Liquid engines, and enhances the reliability of the AI Email Designer and concurrent template editing. It also includes multiple bug fixes for contact management, A/B testing attribution, automation workflows, and database connection stability under load.
Key Highlights
- Broadcast reports now display per-link click breakdowns with unique clicker counts.
- Email rendering performance is improved using engine pooling and compiled regexes.
- AI Email Designer reliability is enhanced with streaming reasoning panels and better error handling.
- Concurrent template edits are now prevented from overwriting each other using revision locking.
- Security update resolves an XSS vulnerability by bumping the echarts dependency.
New Features
- Broadcast reports show per-link click breakdowns (total/unique) per destination URL.
- Templates list now supports searching by name with a debounced, URL-persisted filter.
- AI Email Designer features streaming thinking, immediate preview refreshes, and token limit warnings.
- Template editors now reject stale revisions with a 409 Conflict to prevent silent overwrites.
- One-time authentication emails gain a tri-state tracking_mode (inherit/disabled).
Full Release Notes
- **Feature**: Broadcast reports now show a per-link click breakdown — total clicks and unique clickers per destination URL (UTM parameters kept, since they are commonly used to tell links apart). Clicks are recorded per URL in a new `message_history.clicked_links` JSONB column added by workspace migration v35; recording starts immediately after the upgrade, including for emails already sent (the destination URL is embedded in their tracking tokens), so the breakdown only covers post-upgrade clicks and legacy `/visit` links remain aggregate-only (#339, #311).
- **Feature**: Search templates by name on the templates list — a debounced name search beside the category filter, persisted in the URL, mirroring the broadcasts search (the already-loaded list is filtered client-side).
<img width="1160" height="743" alt="Capture d’écran 2026-07-08 à 11 40 58" src="https://github.com/user-attachments/assets/402c9329-cfba-41b1-bf15-2e7d9f42d171" />
- **Improvement**: Faster email rendering — Liquid engines are now reused across blocks via a pool instead of being rebuilt for every block, and constant regexes are compiled once, cutting per-block Liquid processing time by ~3× (~8× under concurrent sends) during broadcasts (#381).
- **Improvement**: AI Email Designer reliability with reasoning models (e.g. DeepSeek) — thinking now streams into a collapsible panel, the preview refreshes immediately after AI edits, a generated email that doesn't compile surfaces the MJML error instead of silently looking successful, hitting the token limit shows a non-destructive warning instead of wiping the answer, and OpenAI-compatible integrations gain a reasoning-effort selector (#363).
- **Fix**: The contacts multi-select no longer gets out of sync when deleting via the row's ⋯ menu — clicking a dropdown item invisibly toggled the row's selection (the click bubbles through the React portal to the row's select-on-click handler), and a deleted contact was never removed from the selection, leaving a phantom "N contacts selected" bar whose stale emails a later bulk *Add to list* would even re-create via upsert (#374).
- **Fix**: One-time authentication emails (the built-in Supabase notifications: magic link, signup confirmation, invite, recovery, email change) are no longer rewritten through the click-tracking redirect — their links previously depended on the API being up and could be consumed by mailbox link scanners before the user clicked. Transactional notifications gain a tri-state `tracking_mode` (inherit the workspace setting, or disabled — which also suppresses UTM rewriting); an absent field on update keeps the stored value so partial edits cannot wipe an opt-out, and migration v35 sets `disabled` on existing Supabase notifications.
- **Fix**: A/B-test broadcasts no longer attribute most recipients' link clicks and `{{ utm_content }}` values to the wrong variant. When no explicit `utm_content` was set, the first processed recipient's template ID was written back onto the shared broadcast object and frozen for every later recipient — and in batch sends the template data even lagged one recipient behind the rewritten links. `utm_content` is now computed per recipient from the variant actually chosen for them, and `utm_term` is applied consistently across both send paths.
- **Fix**: The automation *Add to List* node no longer fails silently. Its "Subscribed" option stored an invalid `subscribed` status that the backend rejected (only `active`/`pending` are valid), so contact journeys stalled at that node with no visible error. The editor now stores `active`, node configurations are validated when the automation is saved so an invalid status is caught immediately, and a data migration repairs existing automations, contact lists, and timeline entries that still carry `subscribed` (#376).
- **Improvement**: Concurrent template edits no longer silently overwrite each other. Template saves were last-writer-wins, so a save based on a stale revision clobbered newer changes. The editor now sends the revision it was based on and the server rejects a stale-base save with `409 Conflict`, prompting to reload the latest or overwrite. Covers the email template, blog post, and transactional-notification editors (#378).
- **Security**: Bumped the console's `echarts` dependency to 6.1.0 to resolve a cross-site scripting (XSS) advisory (GHSA-fgmj-fm8m-jvvx) flagged by Dependabot.
- **Fix**: Resolved intermittent `pq: password authentication failed` / "failed to get workspace connection" errors under load. The workspace connection manager health-check-pinged the cached pool on every query and evicted, closed, and rebuilt it (re-hitting the `postgres` admin database) on any slow ping or transient blip; it now reuses cached pools and creates them without a global lock (#380).
- **Fix**: Adding a contact (or adding a contact to a list from the details drawer) now refreshes the contacts list immediately instead of requiring a hard page reload. The "Add" contact drawer never invalidated the React Query cache on success, and the "add to list" action only refreshed the contact details — both now invalidate the contacts list (and total count) so the new contact appears right away (#364).