Matthew Meszaros
3bf8d4bdfa
Merge remote-tracking branch 'origin/main' into feature/mailbox-fair-use-allowance
2026-09-04 21:25:53 -07:00
Matthew Meszaros
3596a9770f
feat: make mailboxes truly unlimited under a fair-use allowance of one mailbox per daily send the plan includes (15,000 on Business), drop the 200 hard cap and the 5-per-day connect throttle, open a request-more dialog with inline limit requests and plan upgrade when the allowance is reached, add bulk CSV mailbox import with live progress and a downloadable failed-rows file, reserve attachment storage atomically under a per-org lock so concurrent uploads cannot pass the quota ( #326 ), report storage and the allowance on the billing usage meters, and honor approved daily-send overrides in the sender
2026-09-04 21:00:26 -07:00
Matthew Meszaros
f68918d7f6
Merge remote-tracking branch 'origin/main' into feature/warmbly-cli
2026-09-04 20:14:46 -07:00
Matthew Meszaros
028689fd2e
feat: device-code sign-in for the CLI, with a browser approval page at /cli that mints a scoped API key, self-revocation at DELETE /api-keys/self so a read-only credential can always end itself, and app_url plus websocket_url on /auth/config so a client can find the dashboard and the realtime gateway on a self-hosted layout
2026-09-04 20:14:27 -07:00
Matthew Meszaros
5fd9aedb80
Merge remote-tracking branch 'origin/main' into feature/plan-upgrade-paywall-modal
2026-09-04 09:11:51 -07:00
Matthew Meszaros
d78e4423e4
fix: address the review of the notifications work — bound detached alert delivery to a fixed pool that drops and logs rather than spawning a goroutine per event, never echo a transport error that embeds the webhook URL, treat an emptied channel target as cleared so switching transport cannot post Slack payloads to a Discord URL, emit user.registered for invited signups and the worker alert after a successful reassignment, format Stripe amounts by currency exponent, refuse an upgrade while the plan list is still loading instead of silently opening the portal, apply a fixed discount to the annual invoice rather than to each month, convert proration from minor units, contain focus in the nested enterprise dialog, hide the odometer wheels from assistive tech, and drop the last dedicated-IP row from the pricing matrix
2026-09-04 06:33:20 -07:00
Matthew Meszaros
85f3dad215
feat: send the files a step actually carries by honouring campaign_attachments.sequence_id in the send, test-send and preview paths instead of attaching every campaign file to every step, count them per step in the preflight content score, and refuse an upload naming another campaign's step
2026-09-04 06:16:16 -07:00
Matthew Meszaros
81d6ba4520
Merge remote-tracking branch 'origin/main' into feature/plan-upgrade-paywall-modal
2026-09-04 06:09:14 -07:00
Matthew Meszaros
7d8bb08fa0
fix: Talk to sales now opens an enterprise inquiry form instead of a Stripe portal session a free-tier prospect has no customer for, and correct two clients that could never succeed: the portal sent no body against a required return_url and read a url key the endpoint never returns, and the enterprise inquiry sent name/email/company/message against a handler binding company_name/contact_name/contact_email
2026-09-04 05:20:31 -07:00
Matthew Meszaros
19f8dfbc20
feat: rebuild Billing > Overview as a real control panel with cancel and resume, live usage meters against server-enforced limits, a capability matrix, trial and past-due and pending-cancellation banners; price plan switches with a proration preview on the Plans tab; fix the cancel client that sent no body and always 400'd; stop the Payment tab claiming no card is on file
2026-09-04 04:48:59 -07:00
Matthew Meszaros
483d6c2fe7
Merge remote-tracking branch 'origin/main' into fix/issue-307-test-send-preview-from-name
2026-09-04 03:34:35 -07:00
Matthew Meszaros
aa55b31422
feat: give the campaign step preview a contact and mailbox picker so it renders for a real lead with that sender's signature, the opt-out footer and the attachment list, add a Send test action that mails the saved step through the chosen mailbox, and document both in the sequences guide
2026-09-04 02:58:36 -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
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
579b0ac04c
feat: merge main into the unsubscribe branch again, carrying the UTM campaign columns alongside unsubscribe_mode in every campaign scanner, keeping unsubscribe links out of the moved link tracker and its UTM tagging, and renumbering the opt-out migration to 000124 because main released 000123 for link clicks
2026-09-03 03:43:47 -07:00
Matthew Meszaros
78c4021053
Merge remote-tracking branch 'origin/main' into feat/link-click-attribution
...
# Conflicts:
# docs/content/docs/api/reference/campaigns.mdx
# docs/content/docs/guides/campaigns.mdx
# internal/repository/pg_campaign.go
# internal/repository/pg_campaign_lifecycle.go
# web/src/components/app/campaigns/NewCampaignDialog.tsx
2026-09-03 03:14:19 -07:00
Matthew Meszaros
9485503e03
feat: merge main into the unsubscribe branch, keeping the new campaigns.kind column alongside unsubscribe_mode in every campaign scanner and renumbering the opt-out migration to 000123 because main took 000122 for campaign kind
2026-09-03 01:56:38 -07:00
Matthew Meszaros
5eb92c601c
feat: give every campaign email a working opt-out: a reply-to-opt-out line by default or an unsubscribe link (workspace setting under Settings > Sending with a per-campaign override and a {{.UnsubscribeLink}} variable), signed per-recipient unsubscribe links served on the API origin so the List-Unsubscribe header no longer points at a dead warmbly.com page, a confirm page on GET with RFC 8058 one-click on POST and a resubscribe button, reply opt-out detection through the whole-word compliance lexicon with quoted history stripped, a first-class suppression list (Contacts tab, GET/POST/DELETE /suppressions with address and domain entries, audited removal, contact drawer action), the contact Subscribed flag enforced in campaign routing, migration 000122 with a shared recipient_suppressed() predicate, and docs for all of it
2026-09-03 01:52:40 -07:00
Matthew Meszaros
1cb2b65cd7
Merge remote-tracking branch 'origin/main' into feat/link-click-attribution
2026-09-03 01:42:44 -07:00
Matthew Meszaros
24b7e26fbb
feat: add the one-time email campaign preset from #288 : campaigns.kind column with sequence and one_time values and a kind field on POST /campaigns, GET /campaigns?kind= and an one_time count on /campaigns-overview, a POST /campaigns-estimate endpoint projecting deduplicated segment recipients against the mailbox pool's per-day capacity and finish date, a wizard type picker whose one-time flow runs Basics, Email, Audience, Sending and Send with send-now or scheduled start and the estimate panel and then creates, links segments and starts the campaign, a guard refusing a second email step on a one-time campaign, draft/scheduled/sending/sent wording with a One-time badge and Type filter in the campaigns list, kind on the AI create_campaign_draft tool, honouring text_only in the send and test-email paths so plain-text campaigns ship no HTML part or tracking, and the campaigns, segments, API reference and endpoint docs
2026-09-03 01:42:00 -07:00
Matthew Meszaros
f579497c81
feat: make profile and workspace avatar changes survive a refresh: user avatar upload and remove go through the user service so the cached /auth/me copy is dropped instead of serving the old avatar for 30 minutes, the org list mapper carries avatar_url into the persisted workspace pointer it previously discarded, the avatar mutation hooks patch the cached user, org pointer and org queries with the server's answer before invalidating, all four handlers delete the replaced or removed object under our own avatars/ prefix with millisecond keys so quick replacements cannot collide on an immutable URL, the org avatar removal is audited, the uploader copy stops promising WebP and GIF, and the API docs note that remove deletes the stored file ( fixes #295 )
2026-09-03 01:06:36 -07:00
Matthew Meszaros
99be92f159
feat: attribute every campaign click to the exact link with a per-link click log (email_link_clicks) behind the contact activity timeline, campaign live feed, recent activity and the email_clicked webhook, add per-campaign automatic UTM tagging (utm_tracking with source, medium and campaign overrides, utm_content from the link text) applied at send time to the stored ticket destination, and classify opens and clicks as machine when they arrive within ten seconds of dispatch or when several links of one email are followed within five seconds, so scanner clicks are logged but never count as engagement, fire automations or send webhooks
2026-09-03 01:05:34 -07:00
Matthew Meszaros
290f1f00aa
Merge remote-tracking branch 'origin/main' into feature/public-forms
...
# Conflicts:
# cmd/backend/main.go
# docs/content/docs/guides/workspace-export-import.mdx
2026-09-01 09:29:53 -07:00
Matthew Meszaros
05d74dbe9f
feat: forms v2, a full redesign of the hosted form and its builder: eight one-click themes, card/wide/split layouts with a brand cover panel, classic paged and Typeform-style focus modes split on a new page_break block, logo/cover/background uploads with size, fit and veil controls, an optional header bar that can span the page or sit with the form, a real HSV colour picker and font preview, per-contact personalized links that prefill and attribute without email verification, a render-token gate so the form JSON cannot be scraped without loading the page, funnel analytics with per-page drop-off and identified visitors, a leads-style forms list and responses table, and an organization custom forms domain verified by CNAME with hourly re-checks
2026-09-01 09:27:28 -07:00
Matthew Meszaros
128e314103
Merge remote-tracking branch 'origin/main' into fix/issue-277
2026-09-01 04:03:37 -07:00
Matthew Meszaros
d6ddf1f170
feat: fix implicit-TLS SMTP on 465 and IMAP STARTTLS on 143 behind a stored per-mailbox security mode that accepts any port, stop worker ID churn orphaning mailbox assignments via flock-claimed persistent worker ids, give the unibox a standard mail-folder sidebar (inbox/sent/drafts/archive/spam/trash) backed by a provider-derived folder column, and expose the AI tool registry over REST for non-MCP function-calling agents ( #283 )
2026-09-01 03:53:19 -07:00
Matthew Meszaros
5e87b1bbbf
Merge remote-tracking branch 'origin/main' into feature/public-forms
2026-09-01 01:17:54 -07:00
Matthew Meszaros
60c9e316d9
feat: hosted lead-capture forms end to end: drag-and-drop builder with field settings, design panel, embed/share and submissions tabs in the dashboard, a public TanStack form app (forms/) served by the new standalone forms service (cmd/forms + internal/formserver) on FORMS_DOMAIN with per-form frame-ancestors CSP, honeypot/fill-time/Turnstile/per-IP submit protection and a same-origin JSON API proxying the backend internal API, form submissions creating contacts with categories and campaign enrollment plus realtime, audit, webhook and org-transfer coverage, migration 000114, seed forms, CI jobs, Dockerfile, systemd/nginx/compose manifests and docs
2026-09-01 01:17:51 -07:00
Matthew Meszaros
66a105fb9f
feat: make segments a live campaign audience and round out lead management (issue #277 ): a campaign_segments link table with GET/PUT /campaigns/:id/segments (max 20, replace semantics), immediate enrolment of linked-segment members as leads plus targeted re-syncs on contact writes, segment edits, pin-ins and imports and a 2-minute backend sweep for drift, waking active campaigns and restarting completed ones through the full launch checks; a Segments manager dialog and toolbar button on the campaign Leads tab; Remove from campaign as the row action and a selection-bar bulk action so leads can leave a campaign without deleting the contact; campaign and segment pickers in the CSV import wizard backed by a new segment_ids import option that pins imported rows as include overrides; a delete guard naming linked campaigns; orgtransfer registration, live repo tests and docs for all of it
2026-08-31 08:45:27 -07:00
Matthew Meszaros
896fc60f4d
feat: add the missing mailbox re-verification flow (issue #274 ): POST /emails/onboarding/oauth/reauth/:id re-runs the provider consent for an existing Gmail/Outlook mailbox (login_hint preselect, same-address check, token rewrite keeping the stored refresh token) and PUT /emails/onboarding/smtp-imap/:id replaces SMTP/IMAP credentials after live validation; both resolve the credential-class error rows and reactivate the mailbox onto its worker, the drawer's Needs attention banner gains Re-authorize and Update credentials buttons, and the mailboxes guide documents reconnecting
2026-08-31 05:29:35 -07:00
Matthew Meszaros
7a7c6051bc
Merge remote-tracking branch 'origin/main' into feature/contact-segments
2026-08-30 02:07:56 -07:00
Matthew Meszaros
960f42b846
feat: add the dashboard segment model, API client and react-query hooks for segments, fields, preview, overrides, member modes, contact-side membership and campaign enrolment
2026-08-29 23:45:05 -07:00
Matthew Meszaros
a02ff7c936
feat: address verification overhaul for #264 : MillionVerifier as a pay-as-you-go integration plugin with org-sealed key and automatic fallback to the built-in check, built-in prober gains domain cache, Microsoft/Yahoo fingerprinting, MX fallback, disposable/role sub-statuses and a self-check breaker, imports and POST /contacts accept verification results in any known provider vocabulary with auto-detected columns, verdict provenance and expiry columns (migration 000110), campaigns park at paused_undeliverable with re-verify/send-anyway instead of finishing, POST/GET /contacts/verification bulk actions and overview, launch gate override via acknowledge_list_risk, animated verification marks, banner and settings card in the dashboard, and docs
2026-08-29 23:11:22 -07:00
Matthew Meszaros
2a831e9783
feat: let a linked self-hosted instance sign Google and Microsoft mailboxes in through Warmbly Cloud's own OAuth apps and send with cloud-brokered access tokens: the cloud runs the consent (pool_link_mailboxes.managed, brokered state in Redis, the existing /addresses/*/callback completes it and redirects to the instance's /cloud-oauth/done), keeps the refresh grant, mints short-lived tokens at /pool-link/instance/mailboxes/:id/token and refuses them for revoked links, removed, inactive or blocked mailboxes; the instance mirrors such mailboxes without a credential (cloud_link_mailboxes.managed), ships them to the worker as brokered so goog/msgraph init on a token source that pulls from /api/v1/internal/cloud-link/token/:id, lets the consumer ignore cloud warmup tokens for enrolled mailboxes, and can adopt mailboxes connected directly on the workspace; Add account shows the cloud path and the adoptable list, and the Warmbly Cloud guide documents the model
2026-08-29 09:50:52 -07:00
Matthew Meszaros
d621d9b79b
feat: sign a new account in as part of registration: createAccount returns the user, both the verification-free and the code-confirm paths mint a session through finishLoginAs (2FA challenge included), /auth/register/confirm answers with that session instead of 204, and the dashboard drops into the workspace right after signing up instead of bouncing to the sign-in form
2026-08-29 08:41:34 -07:00
Matthew Meszaros
b75fdcf86c
Merge remote-tracking branch 'origin/main' into feature/self-hosted-warmup-pool-access
...
# Conflicts:
# internal/scheduler/warmup_scheduler.go
2026-08-29 07:10:38 -07:00
Matthew Meszaros
37b60b59d3
feat: let a self-hosted instance warm its mailboxes in the hosted pool: device-code link approved at /connect, instance-token API that enrolls SMTP/IMAP mailboxes as warmup-only accounts (no history import, non-warmup mail dropped), free for 10 mailboxes and unlimited on the seeded $15 pool plan, tier fallback to proven healthy mailboxes when a pool runs thin, local warmup stands down for enrolled mailboxes, Settings > Warmbly Cloud step flow and linked-instances page, docs guide, marketing copy, and fix SetWarmupLifecycle re-reading the row with an org-scoped lookup so every warmup start/pause returned 404
2026-08-29 07:09:04 -07:00
Matthew Meszaros
5821fc2a1c
feat: merge origin/main into the website tracking branch after PR #260 landed 000106_contact_source, renumber the website tracking migration to 000107, and fold the page_hit timeline type, its query source and its Website filter, meta line and expandable details into the reworked contact Activity tab and models from the contact-activity PR
2026-08-29 04:38:35 -07:00
Matthew Meszaros
a4739b63ec
feat: add first-touch contact source attribution (migration 000106: contacts.source with a CHECK, source_detail, first_seen_at, existing rows stamped unknown) stamped at every creation site (dashboard manual/campaign, file import, Google Sheets sync, API key, AI assistant), write contact_created / campaign_added / campaign_removed / category_added / category_removed lifecycle events into contact_activities inside the same transactions as the links and merge them into the contact timeline, refactor FindNextRoutedPair's routing into a campaignRouter shared with a per-contact RouteContact and split CalculateNextCampaignTime into campaignSenders + placeCampaignSend so a read-only PreviewContactSend derives a contact's next step through the scheduler's own constraints, expose it as GET /contacts/:id/campaigns behind the contact service, render a per-campaign state panel, Campaigns/Lifecycle filter chips and expandable event rows in the Activity tab plus a Source section on Overview, cover it with TestLive* for the timeline events and the next-action preview (step wait, sending window, paused), and document it in the contacts, campaigns and API reference docs (issue #255 )
2026-08-29 03:28:16 -07:00
Matthew Meszaros
27630eec0a
feat: add website visitor tracking for issue #255 with migration 000106 (website_tracking_settings, website_visitors, website_page_hits, all registered in the orgtransfer spec), a consent-gated dependency-free tracking.js served by the Rust tracking service with a rate-limited, size-capped, prefetch-filtered POST /p ingest that forwards to a new backend internal page-hits endpoint for server-side user-agent and GeoIP enrichment, contact identification only through the click ticket the redirect appends to registered hosts, a per-workspace retention job, page_hit events with an expandable detail view in the contact Activity timeline, a Settings > Website tracking page for the snippet and consent, location and retention configuration, realtime PAGE_HIT fanout, and a website tracking guide plus endpoint, export and configuration docs
2026-08-29 03:25:50 -07:00
Matthew Meszaros
35e8f95f2a
feat: make a release from the mailbox hold land where the rebalancer would put it by running lifecycle.Decide against the mailbox's current warmup health through a new GetLifecycleCandidate repository read, so an unhealthy mailbox goes straight to resting instead of sending cold for up to an hour before the hourly pass rests it again, report that outcome in the drawer toast and the API reference, and condense the new hold comments in the email service, backend wiring, repository, hook, client and drawer to the one-line form the repo convention asks for
2026-08-28 23:30:15 -07:00
Matthew Meszaros
5c832461b0
feat: wire the unreachable reserve send-lifecycle state to a real per-mailbox hold (POST /emails/:id/hold and /release behind manage_emails and WRITE_EMAILS, SetSendHold on the email service forcing the lifecycle past the rebalancer guard, a Hold from campaigns toggle on the mailbox drawer's Overview tab with the reserve notice rewritten to point at it, warmblyctl mailbox hold/release, and docs in the mailboxes guide, API reference and scope map), and drop the warming state nothing ever set from the model, the web type, the docs table and the DB check via migration 000104, which folds any legacy warming row back to active
2026-08-28 22:47:31 -07:00
Matthew Meszaros
8ef6ee5545
feat: measure an uploaded list at import, and say what it looks like ( #236 )
...
* feat: refuse a launch whose list is known to be largely undeliverable
* feat: count the deliverable audience in SQL instead of subtracting two overlapping totals, surface the unverified advice, and stop a query error skipping the gate silently: a contact can be both suppressed and unsubscribed, so Total minus both removed it twice and inflated every share computed against the remainder, which for a fully overlapping list produced a negative deliverable clamped to zero; the unverified-list branch set a summary and remediation but neither Block nor Warn, and a preflight report only surfaces checks that did not pass, so that advice could never be displayed; and a failed audience query was treated as a pass, which is fail-open on a safety gate without even a log line saying the check did not run
* feat: measure an uploaded list at import time and report what it looks like
* feat: document the import assessment and show it in the wizard
* feat: count only sendable leads as invalid, and stop guessing which column held a malformed address: an invalid lead that was also suppressed sat in the numerator while Deliverable excluded it from the denominator, so a campaign whose sendable list was clean could project above 100% and be refused; every verification count now shares the deliverable predicate, and a row whose MAPPED address will not parse is recorded as malformed directly rather than scanning other cells for an at sign, which could pick up a notes field
2026-08-28 11:50:48 -07:00
Matthew Meszaros
b019174910
feat: give an organization one fused abuse posture ( #233 )
...
* feat: give an organization one fused abuse posture, because every existing control watches a single subject and an actor slightly wrong on several axes sits under all of them: organizations gains risk_state, risk_score, risk_reason and an append-only risk_signals evidence blob, modelled on the warmup participant health machine that already works rather than a second vocabulary for the same idea; restricted cuts per-mailbox cold volume to a quarter and forces the free warmup pool so a risky tenant cannot spend the paid pool's shared reputation, suspended stops sending at the send gate, and watch deliberately changes nothing a customer can feel so evidence accumulates before anything is taken away; an operator's suspension outranks the derived band so a detector clearing cannot release a workspace a human suspended, transitions ride the audit spine to every teammate's dashboard, a banner says which limit is active and why rather than letting volume drop silently, and the posture never travels in a workspace archive since it is one platform's verdict reached from evidence the destination never saw
* feat: make the suspension actually stop sending, and emit the audit transitions the PR claimed: emailsend.SendEmail is only the manual and API path, so campaign and warmup sends went nowhere near the gate and a suspended workspace kept sending on its schedule, while the restricted multiplier floored every mailbox at one a day which quietly turned suspension into a trickle rather than a stop; the campaign scheduler now defers the whole campaign with a reason and the warmup task skips as org-suspended, since warmup is outbound mail from the same domains; separately the band change emitted no audit entry at all despite the entity type and the frontend spine entry both existing, so no banner moved for a teammate and there was no trail of who was restricted when, and only a real transition is logged so a detector re-recording the same finding cannot fill the feed; one of my own live tests also asserted how far out a slot lands, which depends on the hour the suite runs, and now asserts the property it was about
* chore: drop a test that belongs to the signup-risk change and reached this branch by mistake, where the type it exercises does not exist
2026-08-28 11:20:19 -07:00
Matthew Meszaros
ca5acb5aaf
feat: score campaign copy as sent, not just as written ( #229 )
...
* feat: score campaign copy where it actually matters, on the message the recipient receives rather than only the template in the editor: warmlint.Score already ran at save time through /templates/score, but nothing checked the copy after merge fields, spintax, A/B selection and AI blocks resolved, which is exactly where a clean template becomes Hi comma or picks the one spammy spintax branch; the send path now scores the rendered message and writes one campaign-feed warning per step per day rather than one per recipient, RunPreflight gains the same check across every step, warmlint gains image-heavy, many-images and attachment heuristics, and the whole thing is advisory behind Settings > Sending > Content checks; the launch dialog also finally calls POST /campaigns/:id/preflight, whose report had no UI at all and so never reached the person about to send
* feat: fix three defects the review found in the content lint: a preflight run that could not read the campaign's steps left the worst score at 100 and reported the check as PASSED, which is the one outcome a check that did not run must never produce, so it now reports failed with the reason; the send-time path read organization settings while preflight read campaign-effective ones, so a campaign that turned the check off or moved its floor was still warned, and EffectiveSettings is now exposed and used by both; and the check-then-insert dedupe let concurrent recipients of the same low-scoring step each find nothing and each write, so it becomes one advisory-locked conditional insert proven by twelve racers producing exactly one feed entry
2026-08-28 10:29:39 -07:00
Matthew Meszaros
b83a1a3ce5
feat: make send-time optimization actually schedule sends ( #226 )
...
* feat: make send-time optimization actually schedule sends, instead of being a documented setting with no caller: advanced.OptimizeSendTime had zero call sites anywhere in the codebase, so an org that enabled send_time_optimization through PATCH /outreach/settings changed nothing about when its campaign mail left, and the API reference said so in a callout; the campaign scheduler now resolves the recipient's timezone from the contact's timezone custom field, then the country-code suffix of its email domain, then the org fallback, and holds the slot until that clock reaches a preferred hour, raising hardFloor so the task handler reschedules rather than sending immediately, the snap is forward-only and never crosses the campaign end date, the default flips to off so no existing workspace silently re-times its sends, and Settings > Sending gives the block its first UI
* feat: stop the recipient-hour gate deferring a send forever, and fix the window parser that silently disabled every campaign schedule: recipientSlot now searches for a moment BOTH calendars accept and yields when they never meet, because raising a hard floor at an hour the sender cannot serve made every tick re-derive it, defer, wake in the sender's window and defer again; separately parseTimeOfDay accepted only the 15:04 layout while start_time, end_time, warmup_start_time and warmup_end_time are Postgres time columns pgx renders as 09:00:00.000000, so every read parsed to 0 and effectiveWindows read that as unconstrained, leaving both the campaign sending window and its day-of-week gate off for every campaign on the legacy fields and pinning warmup to its 08:00-20:00 fallbacks; Settings > Sending also gains the MANAGE_SETTINGS gate a direct visit needs
2026-08-28 09:46:39 -07:00
Matthew Meszaros
029bc27bfd
feat: bind a browser sign-in to the browser that started it, so a handoff link cannot be forwarded: one-time state proves the callback answers a request this server made, not one THIS browser made, so anyone could run the flow against their own Google or OIDC account and send the resulting URL to someone else, whose browser would then hold the sender's session (RFC 9700 4.7.1); begin now mints a binding secret that never reaches the provider and never appears in a URL, the callback carries it into the handoff, and the exchange refuses a collection that cannot present it with sso_wrong_browser, while the comments this PR added are condensed to the constraint they exist to state
2026-08-28 01:44:39 -07:00
Matthew Meszaros
9ab42cfd65
feat: build the browser half of social sign-in, which was never wired: GOOGLE_CLIENT_ID was read at boot and made the login screen render a Google button, but the button opened a popup at /auth/google/login which no route served, and authService.GoogleAuth/AppleAuth had no caller anywhere in the codebase; internal/app/socialauth now runs Google and Apple through the flow generic OIDC already used (one-time state, PKCE, nonce, id_token verified against the provider JWKS, identity keyed on issuer and subject, JIT provisioning, the ban and 2FA gates), the redirect URI defaults to API_PUBLIC_URL/v1/auth/<provider>/callback and is logged at boot because registering the dashboard origin instead is the mistake that produces a valid OAuth client and a dead button, /auth/config advertises only providers the backend can actually complete, the SSO landing page no longer swallows a two_fa_required response, and OIDC_PROVIDER_NAME finally reaches the button it documents
2026-08-28 01:33:08 -07:00
Matthew Meszaros
995ca1697b
feat: teach the shared TextInput primitive an invalid flag that paints our own red hairline and sets aria-invalid plus a title for the reason, so a rejected value is shown in the dashboard's theme rather than a browser default, and declare errors_truncated on the ImportResult client type
2026-08-27 03:45:46 -07:00
Matthew Meszaros
d396f53fc4
feat: add Delete and Duplicate campaign actions to the dashboard (issue #185 ): every campaign row and the detail header get a ⋯ menu (Edit, Duplicate, Start/Pause, Delete) plus a Delete card at the bottom of Settings, all permission-gated with a confirm that spells out what goes; DELETE /campaigns/:id is now organization-scoped instead of user_id-scoped so teammates can delete, runs in one transaction that also deletes the campaign's pending tasks and cancels a wakeup tick claimed at that moment (campaign_tasks only nulls its link, so those rows kept firing), removes attachment objects and publishes CAMPAIGN_DELETED so a teammate's open detail page is sent back to the list; new POST /campaigns/:id/duplicate copies the campaign row as a draft with steps and their branch graph rewired onto new step ids, tags, folders, senders with rotation reset, A/B variants, advanced settings and attachments (quota-checked, blobs undone if the copy fails) and none of the leads, progress, logs, counters, ramp level, guardrail trip or past dates, naming it (copy)/(copy N) inside the 50 byte cap without splitting runes; a claimed campaign tick whose campaign vanished now ends the chain instead of staying active forever; covered by TestLiveCampaignLifecycle* against real SQL, RemapBranchTargets and duplicateName unit tests and a react-query vitest for the list cache, with API reference, endpoint map and campaigns guide updated
2026-08-25 08:54:51 -07:00