Commit Graph
2036 Commits
Author SHA1 Message Date
Matthew Meszaros f042e307e5 feat: rewrite the installer's three A && B || C constructs as guard clauses so shellcheck's SC2015 is satisfied on the CI runner's version, and print the shellcheck version in check-installer.sh 2026-09-04 05:53:16 -07:00
Matthew Meszaros d68bbcd2ab feat: add a one-command self-host installer at warmbly.com/install.sh with an interactive data-control wizard, give docker-compose.yml image keys and per-store volume variables, add an image-mode updater, move engagement/form/audit retention into instance settings, and add warmblyctl backup/restore 2026-09-04 05:49:54 -07:00
Matthew Meszaros 068b34751b Merge pull request #312 from warmbly/fix/campaign-daily-budget-deferrals
Campaign scheduler: count only real sends against the daily budget and defer at cap instead of pausing (#306)
2026-09-04 03:13:50 -07:00
Matthew Meszaros 64e6e3a502 Merge remote-tracking branch 'origin/main' into fix/campaign-daily-budget-deferrals 2026-09-04 03:06:45 -07:00
Matthew Meszaros 14827e5123 Merge pull request #311 from warmbly/fix/contact-timeline-opaque-cursor
Contact timeline: opaque (at, source, id) cursor with a tie-break in every source
2026-09-04 03:05:49 -07:00
Matthew Meszaros 2a19bf0519 Merge remote-tracking branch 'origin/main' into fix/campaign-daily-budget-deferrals 2026-09-04 02:59:01 -07:00
Matthew Meszaros 5c9d365163 feat: pick a closed-hours mailbox that stays closed for the next ten minutes so the scheduling pass cannot run after it opens, and assert the mixed-pool deferral lands on the reopening from both sides instead of accepting any earlier time 2026-09-04 02:59:00 -07:00
Matthew Meszaros eb85224cc8 Merge remote-tracking branch 'origin/main' into fix/contact-timeline-opaque-cursor 2026-09-04 02:57:59 -07:00
Matthew Meszaros 293226101a Merge pull request #313 from warmbly/fix/campaign-linked-segment-display
Show linked segments on the campaign Leads tab, explain empty lists, add export to leads and segments
2026-09-03 20:54:15 -07:00
Matthew Meszaros 850ff2b6a2 feat: lock the campaign row for update while replacing its linked segments so two concurrent replacements cannot commit the union of their sets, and return the status read under that lock from ReplaceForCampaign so the wake or restart reaction no longer depends on a post-commit link lookup that a concurrent detach could empty 2026-09-03 20:46:27 -07:00
Matthew Meszaros ab0c756aa2 feat: show a campaign's linked segments on the Leads tab as chips with live enrolled and held-out counts that filter the list, explain an empty list caused by an empty segment or hand-removed members and add them back in one click, add a scope-aware Export to campaign leads and segment members with a campaign-ready preset that carries lead status and engagement, write the links and the enrolment of PUT /campaigns/:id/segments in one transaction so a failed enrolment changes nothing instead of answering added 0, restart a completed campaign when a one-shot enrol adds leads, clear the manual-removal record when a contact is added by hand, count every link's members in one scan, audit sweep enrolments so open Leads tabs refresh, and document it in the campaigns and segments guides and the API reference 2026-09-03 20:38:27 -07:00
Matthew Meszaros 8aaf9e8c8c feat: say in the contacts API reference and the OpenAPI 400 description that a before value on the contact timeline that is not an RFC 3339 timestamp is rejected with a 400 rather than accepted 2026-09-03 20:28:51 -07:00
Matthew Meszaros 4bc971eecb feat: resume a campaign whose pool mixes capped and hours-closed mailboxes at the earlier of tomorrow and the closed mailbox's reopening, log the daily cap only when every usable mailbox is capped, and add the mixed-pool live regression 2026-09-03 20:26:21 -07:00
Matthew Meszaros 2f32b1b8c3 feat: bring the ContactTimelineEvent OpenAPI schema up to the real event shape with the lifecycle, form_submitted and page_hit types and the machine, link, origin, category, form and page_hit fields backed by new ContactLinkClick, EngagementOrigin and WebsitePageHit schemas, and list form_submitted and page_hit with their payloads in the contacts API reference 2026-09-03 20:21:42 -07:00
Matthew Meszaros 0981635c3a feat: reject a contact timeline cursor whose source rank names no merged table with a 400 in the handler through ContactTimelineSource.Valid, since rank zero is reserved for the legacy before bound and a rank past the last source would re-admit the events at the cursor's instant 2026-09-03 20:21:42 -07:00
Matthew Meszaros 83b0a5ede3 feat: add the cursor query parameter to the contact timeline operation in the OpenAPI spec, mark before as deprecated, describe the invalid cursor and limit 400s, and reference the shared Pagination schema from ContactTimelineResult 2026-09-03 20:19:16 -07:00
Matthew Meszaros eeed44f2ed feat: document the contact timeline's cursor parameter and pagination envelope in the contacts API reference, mark before as deprecated with why a bare timestamp can skip events at a page boundary, and say that a bad cursor or limit is a 400 2026-09-03 20:19:16 -07:00
Matthew Meszaros 8503ad4f37 feat: page the dashboard contact timeline with the server's opaque cursor from pagination.next_cursor instead of deriving a bare timestamp from the last event, in the listContactTimeline client and the useContactTimeline infinite query 2026-09-03 20:19:16 -07:00
Matthew Meszaros 5dbbaad5b5 feat: add the pagination envelope with its opaque next_cursor to the dashboard's ContactTimelineResult type and note that the top-level has_more only mirrors it 2026-09-03 20:19:16 -07:00
Matthew Meszaros 630dd4cab1 feat: add a live repository test that walks ten timeline events, eight of them at one instant across progress stamps, notes and activities, in pages of three and proves nothing is skipped or repeated, each cursor is the last event's key, the final page carries no cursor, and before still means strictly older 2026-09-03 20:19:16 -07:00
Matthew Meszaros a4665579de feat: accept an opaque cursor on GET /contacts/:id/timeline, keep before as a deprecated alias that maps onto the keyset at rank zero, and answer an invalid cursor, before or limit with a 400 instead of silently ignoring it 2026-09-03 20:19:16 -07:00
Matthew Meszaros 88d8a2ba8a feat: page every contact timeline source on a row comparison against the (at, source, id) cursor, unnest campaign progress stamps to one row per event so the limit and the logged-open and logged-click suppression apply per event, fetch one row past the page from every source, return all matching suppression entries, and encode the next cursor from the last event's key 2026-09-03 20:19:16 -07:00
Matthew Meszaros 18a1862d82 feat: rank the contact timeline's sources in models.ContactTimelineSource, give every event a ContactTimelineKey (at, source, id) with the newest-first comparison the merged sort uses, and add the standard pagination envelope to ContactTimelineResult while keeping has_more as a mirror 2026-09-03 20:19:16 -07:00
Matthew Meszaros 407a85ecf9 feat: cover the merged-feed cursor codec with a round-trip test that keeps sub-second precision and rejects a bare timestamp, a wrong-version token, bad base64 and trailing garbage 2026-09-03 20:19:16 -07:00
Matthew Meszaros 0cb4f312a0 feat: add a versioned opaque cursor codec for merged feeds to internal/utils/paging that carries an (at, source rank, id) keyset position and decodes a malformed or wrong-version token to a 400 2026-09-03 20:19:16 -07:00
Matthew Meszaros c298d898a7 feat: document in the campaigns guide what counts against a mailbox's daily budget and that a capped campaign waits for the next day rather than pausing 2026-09-03 20:18:43 -07:00
Matthew Meszaros 1c88c2196b feat: add live tests proving wake-ups leave the daily budget and min-gap untouched, real sends still count, and a capped campaign parks a wake-up with a single daily log line instead of pausing 2026-09-03 20:18:43 -07:00
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 711be1f8c2 feat: defer a campaign whose mailboxes are all at their daily cap, outside their own hours, resting, or held by warmup health instead of auto-pausing it, replace the unreachable push-to-tomorrow recompute, and log the daily-cap and unavailable-pool decisions once per UTC day 2026-09-03 20:18:43 -07:00
Matthew Meszaros 620e89d528 feat: exclude action and wait steps from the workspace daily email limit by joining the step kind, since they stamp sent_at for routing without sending mail 2026-09-03 20:18:43 -07:00
Matthew Meszaros 3204cf41bd feat: apply the reserved-or-confirmed send filter to the workspace sent-today count, the advisor mailbox volume windows, and the deliverability dashboard denominator so campaign wake-ups stop inflating sent totals 2026-09-03 20:18:43 -07:00
Matthew Meszaros ec9a3147b6 feat: count a completed campaign task as a mailbox send only when it holds a step reservation or a worker-confirmed Message-ID, so the chain's deferral and pause wake-ups no longer spend the daily budget or reset the min-gap and rotation clocks (issue #306) 2026-09-03 20:18:43 -07:00
Matthew Meszaros 37d328c86a feat: add a partial index on campaign_contact_progress.dispatch_task_id so the daily send counters can look a task's reservation up without scanning progress 2026-09-03 20:18:43 -07:00
Matthew Meszaros febe324243 Merge pull request #301 from warmbly/fix/click-without-open-tracking
Count a click as an open, label scanner clicks, and record where every open and click came from
v0.3.1
2026-09-03 07:19:21 -07:00
Matthew Meszaros b9e81b6947 feat: fetch one row past the page from the per-event open and click sources of the contact timeline so a page filled by either source alone still reports that more events follow 2026-09-03 06:14:14 -07:00
Matthew Meszaros dc04ea1b52 feat: match a logged open to the step's first open inside a one-minute window on both sides so an unrelated older row cannot hide the summary event, and say in the contacts API reference that only a person's click counts as an open in the contact engagement totals 2026-09-03 05:57:31 -07:00
Matthew Meszaros 611704b365 feat: count only a person's opens in the contact engagement totals and last-opened time as analytics already does, keep a step's legacy first open on the timeline when only later opens were logged per event, and say so in the contacts API reference 2026-09-03 05:43:31 -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 e97477a718 feat: renumber the engagement origin and pending index migrations to 000125 and 000126 because main released 000124 for unsubscribe opt-out while this branch was open 2026-09-03 05:22:05 -07:00
Matthew Meszaros 0354e51a7a Merge pull request #303 from warmbly/feature/self-hosted-update-banner
Self-hosted update indicator and one-click updates
2026-09-03 05:13:26 -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 62f538da5b Merge pull request #302 from warmbly/feat/unsubscribe-opt-out
feat: unsubscribe opt-out in every campaign email, signed unsubscribe links, and a first-class suppression list
2026-09-03 05:02:43 -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 46f61f6ad8 feat: leave a link whose URL has no host untouched by click tracking, since a ticket for it could never redirect 2026-09-03 04:00:33 -07:00
Matthew Meszaros 6830e3277d feat: keep the tracking-domain exclusion when link tracking falls back to UTM-only tagging, leave links the URL parser rejects untouched instead of minting a dead click ticket, and say in the campaigns guide that the sequence versus one-time choice is fixed at creation 2026-09-03 03:53: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