Commit Graph
20 Commits
Author SHA1 Message Date
Matthew Meszaros 1018068942 feat: stop recipient suppression and the entitlement gate being skipped when a campaign has no organization: the send path now fails closed (an orgless campaign is paused with the reason in its activity feed instead of mailing an unsubscribed, bounced or complained address, since routing's own suppression filter joins on the campaign's organization_id and matches nothing when it is NULL), the same tenancy gate covers warmup and unibox sends and an orgless mailbox resolves to the free pool rather than defaulting into the paid one, the state is no longer creatable (sequenceRepository.Create inherits organization_id from its campaign instead of inserting NULL, campaign creation and mailbox onboarding refuse without a workspace via errx.ErrNoOrganization, guardInboxLimit / guardMailboxThrottle / the campaign creation throttle no longer treat a missing org as exempt, and a new session resolves the user's default workspace so the orgless session that produced these rows cannot exist), and migration 000092 backfills then sets organization_id NOT NULL on campaigns, contacts, email_accounts and sequences plus sessions.current_organization_id, provisioning a recovery workspace for any user with none so no row is deleted to satisfy the constraint (live-tested in TestLiveOrglessCampaignDoesNotSendToSuppressedRecipient, TestLiveSuppressedRecipientIsSkipped, TestLiveCampaignRequiresAnOrganization and TestLiveHealthyCampaignStillSends) 2026-08-24 08:47:08 -07:00
Matthew Meszaros c3066f9cc9 feat: unbox campaign start dates and make follow-up pacing real: accept today as "start now" and let an explicit null clear start/end dates on PATCH /campaigns (models.NullableTime distinguishes absent from null, which used to silently no-op while the error message told users to send null), reschedule an active campaign's parked wakeup when any schedule field changes so clearing a future start date takes effect immediately instead of at the old slot, let a completed campaign be started again and turn the past-end-date start 500 into a clear 400, gate the campaign task on the step's hard-constraint floor (wait_after, start date, windows, day capacity, mailbox min-gap) via ErrCampaignDeferred so an early successor tick can no longer send a wait-3-days follow-up seconds after step one (live-tested in TestLiveFollowUpWaitIsHonored), disable past days in the schedule date picker, and fix the sandbox seed leaving worker 1a01 free-tier after make seed which unassigned the paid org's mailboxes and failed every send 2026-08-24 05:20:26 -07:00
Matthew Meszaros fe9a21a79f feat: stop a freshly connected mailbox being silently excluded from every campaign send, by making an unset mailbox timezone representable as the empty string the campaign scheduler already checks for, since email_accounts.timezone defaulted to 'UTC' while campaigns.timezone defaults to 'Europe/London' and nothing in the OAuth or SMTP onboarding paths ever set either, so a brand new mailbox looked deliberately placed in UTC, was compared against the differing campaign zone and dropped by the hardcoded 8am-8pm business-hours gate whenever the current UTC hour fell outside it, emptying the candidate pool and failing the campaign start, adding a migration that changes the column default and converts existing 'UTC' rows because until now no API field, dashboard control or onboarding path could set that column at all so every such row is the old default rather than a choice, adding the missing Timezone field to UpdateEmail with IANA validation so the setting the sending-behaviour UI already tells people to change is finally reachable and an unloadable zone is rejected instead of being silently coerced to UTC by the scheduler, and replacing the misleading 'no active email accounts found for campaign's email tags' response for a pool that exists but is entirely gated out with a distinct message naming the real cause, via an ErrNoEligibleMailbox that wraps ErrNoEmailAccounts so the three callers that pause a campaign on it are unaffected (#126) 2026-08-16 07:46:37 +02:00
Matthew Meszaros 734cb5fe08 feat: make self-hosted onboarding survivable by fixing invite_only, which could not onboard anyone (the accept route is JWT-only, so redeeming the invitation that would create your account required already having one, making the self-host default silently identical to fully closed), threading the invitation token through registration so an invited person lands in the inviting organization instead of a stray workspace, gating SSO just-in-time provisioning behind DISABLE_REGISTRATION (it bypassed the gate entirely, so an instance set to true was still open to anyone the IdP would assert) with SSO_AUTO_PROVISION as the opt-out, correcting the OIDC redirect URL that pointed at /api/v1 against a route at /v1 and 404'd every SSO login, scoping the first-launch exemption so it no longer overrides an explicit lockdown, preserving the remaining TTL when restoring a losing setup token so a public endpoint cannot hold the claim window open forever, replacing a generic 403 with typed registration_invite_only, registration_closed, invitation_invalid, setup_token_invalid and setup_already_complete codes that name the next step, logging why no claim link was issued on an already-claimed instance instead of staying silent, adding a warmblyctl operator CLI (status with health checks and a non-zero exit, reissuable setup-link, user create/list/reset-password/grant-admin/revoke-admin/disable-2fa, hash-password) so a locked-out operator no longer needs hand-written psql, adding read-only instance configuration over 104 environment variables with structural secret redaction and fingerprints, 35 health checks, a database-backed settings tier for the three keys no environment variable owns, hiding the signup form when the config already says invite_only rather than failing the whole form with a toast, and documenting first run, accounts and access, configuration, instance health and troubleshooting alongside the root .env.example the README told operators to write but never shipped (#114) 2026-08-16 05:58:11 +02:00
Matthew Meszaros 0ae4db2c41 feat: make self-hosted auth work without a mail relay by rewriting the platform SMTP transport with real AUTH and TLS (it did neither, so SMTP_USERNAME/SMTP_PASSWORD were dead and every documented relay was unreachable), adding MAIL_TRANSPORT=smtp|log|ses with a log transport that prints codes so a fresh install can sign in with no relay, demoting the emailed login code to AUTH_LOGIN_CODE=always|new_device|off (off on self-host, per NIST SP 800-63B and OWASP ASVS), claiming the first owner through a single-use setup link or WARMBLY_BOOTSTRAP_* instead of register-then-psql, deriving every emailed URL from APP_URL rather than a hardcoded app.warmbly.com that leaked live reset tokens to the vendor, fixing the confirm hooks that read path params against paramless routes and broke login, register and reset confirmation in the dashboard everywhere, adding generic OIDC with PKCE, one-time state, verified nonce and (issuer,subject) identity binding, enforcing 2FA on the social paths that skipped it, adding a per-IP limiter and trusted-proxy handling to the unthrottled auth group, refusing boot on the published default secrets, and dropping mailpit from the default stack (#99) 2026-08-14 14:57:09 +02:00
Matthew Meszaros 8bd2c2b57a feat: make self-hosting work end to end and rewrite the guide around what was tested (#97) 2026-08-13 09:47:46 +02:00
Matthew Meszaros 5e6287c920 feat: add the Advisor, continuous sending checks surfaced on the row they are about (#86)
* feat: index advisor findings by subject and parent entity so a list page fetches its whole surface once and every row resolves its own advice from the shared cache instead of firing a request per row

* feat: rebuild the advisor fix drawer as a three-screen resolution flow (why it fired with the measured evidence, the exact before and after, then an animated outcome with undo) with a progress rail and direction-aware transitions, and deep-link manual fixes to the screen where they are made

* feat: add AdvisorRowFlag, the inline per-row advisor indicator that renders on the mailbox or campaign the problem is about and opens that row's findings in an anchored panel instead of making the reader join a card list against a table

* feat: add AdvisorSummaryBar, a one-line collapsible page summary that replaces the stack of advisor cards above a list, counts the distinct rows implicated rather than the findings, and forces itself open only for critical or workspace-level advice no row flag can carry

* feat: put advisor advice on the mailbox row it is about in the accounts list, replace the card stack above the table with the collapsible summary bar, and support ?mailbox=<id> so a finding can deep-link straight to the mailbox detail instead of the top of the list

* feat: flag advisor findings on the campaign row in the campaigns list, including step-level copy problems which index onto their parent campaign since a step has no row of its own, and add the collapsible summary bar above the list

* feat: move the deliverability and contacts pages onto the collapsible advisor summary bar so their findings stop pushing the numbers they describe below the fold

* feat: add an ordered Steps field to advisor findings, persisted as text[] and always refreshed from the current build, and write real how-to steps for the deliverability checks that have no one-click fix (bounce rate, spam placement, tracking domain, and per-record SPF/DKIM/DMARC instructions)

* feat: write ordered how-to steps for the manual advisor findings where the remedy alone leaves someone stuck (broken template syntax, missing first-name fallback, unsubscribed contacts still enrolled, a campaign with no resolvable sender, and a mailbox that lost warmup pool standing) and correct the personalization detail that named a merge syntax this product does not use

* feat: show a mailbox's advisor findings at the top of its detail drawer, which is where both the row flag and the ?mailbox deep link now land

* feat: open the resolution flow from findings that have no one-click fix too, since the ordered how-to lives there and a card with no Fix button previously left the steps unreachable

* docs: document the per-row advisor flags, the collapsible page summary, the three-screen resolution flow, and the ordered manual steps for findings with no one-click fix

* feat: align the advisor summary bar to the px-5 page gutter used by SectionBar and the list rows on all four surfaces, instead of sitting flush against the edge while the table it describes is indented

* fix: stop the resolution drawer collapsing to zero height between screens by switching the step transition to popLayout with a layout-animated container, so the dialog resizes into the next screen instead of snapping shut and reopening

* feat: wire the advisor repository, narrator, service, tool registration, and background runner into the backend boot path so findings evaluate on a schedule and the assistant can read them

* docs: register the advisor guide in the sidebar, add its endpoint scope table to the API reference, and document the sandbox advisor showcase

* fix: darken the advisor nav badge to solid orange-600 on white instead of a pale amber-100 chip that read as a disabled control beside the sidebar's saturated indicators, and drop the critical badge to rose-600 so the two stay in the same weight class

* fix: use orange-500 for the advisor nav badge, matching the high-severity dot on the row it points at, rather than the darker orange-600

* feat: add an Auto safety class to advisor actions and mark the seven fixes autopilot may apply unattended (the cap cuts, the send-gap widen, the campaign limit matches, and the unsubscribe header), with a test pinning the boundary so nothing that halts sending or generates new outbound mail can drift into it

* feat: add advisor autopilot, which applies the auto-safe fixes unattended as the member who switched it on, resolving their live permissions each run so it fails closed when they leave the org, bounded to 10 changes per evaluation and audited per fix like any hand-made change

* feat: add the advisor agent fix, a bounded per-finding agent run that resolves the problems a settings change cannot (broken template syntax, bulk-reading copy, shared-inbox lists) as the calling member inside a tool allowlist scoped to the finding's category, metered per iteration and marked applied only when it actually called a write tool

* feat: surface autopilot and the agent fix in the dashboard, adding the workspace toggle that names exactly which changes it may make, an Auto chip on the findings it is allowed to take, and an agent-fix path in the resolution drawer that reports the tools it actually called rather than only its own account of them

* docs: document the agent fix and autopilot, naming the exact set of changes autopilot may make, that it acts as the member who enabled it and stops when they leave, and why the agent-fix endpoint is JWT only

* fix: gate the agent fix per detector instead of per category, so a missing DMARC record no longer offers a Fix-with-agent button it can never satisfy and then reports failure; findings whose fix lives in DNS or a provider console now show their manual steps, and the client is told which is which via agent_fixable

* feat: soften the advisor surfaces to translucent washes, replacing the filled nav badge with a tinted pill that carries its colour in the text, frosting the row panel and the resolution drawer, and turning the severity chips and cards into layers the page shows through

* docs: correct the agent-fix scope to name the findings it cannot resolve, and why a DNS record shows steps instead of a button

* feat: ship the actual DNS records for the findings that live outside the platform, with the provider's SPF include resolved, the DMARC record scoped to the sending domain and starting at p=none, the DKIM host plus the console that generates its value, and a tracking CNAME pointing at this install's own tracking host

* feat: render advisor snippets as labelled copy-button rows so a DNS record is one click per field rather than a text-selection exercise, with no copy affordance on a value the server could not supply

* docs: document the pasteable DNS records and the guarantee that every check offers a fix, an agent, or ordered steps

* fix: bump golang.org/x/text to 0.39.0 to clear CVE-2026-56852, a HIGH-severity infinite loop in norm.Iter that Trivy started failing the security scan on
2026-07-30 17:15:09 +02:00
Matthew Meszaros a20ba926cb feat: add native Apple/Google sign-in to the backend - POST /auth/apple and /auth/google exchange provider-signed ID tokens for sessions, GET /auth/providers exposes configured providers for app discovery, with a JWKS-verifying idtoken package, auth config, stable error codes, and API/security docs 2026-07-07 05:56:35 +02:00
Matthew Meszaros da82993846 feat: add campaign workflow backend support
Add sequence action-node storage and execution, template conditional rendering, lead progress state, profile updates, webhook fan-out throttling, and supporting repository fixes.
2026-06-06 07:49:52 +02:00
Matthew Meszaros 2a2e248ad9 feat: add campaign sequence branching backend
Store per-step branching conditions, validate branch targets, and select campaign contact steps by following the flow graph. Preserve linear progression for steps without branch conditions.
2026-06-05 06:02:55 +02:00
Matthew Meszaros 218af13dd9 feat: add session management
Add authenticated session listing and revocation APIs, track the auth provider on sessions, and expose active session controls in account security settings.
2026-06-01 05:53:49 +02:00
Matthew Meszaros 87f9bf58b0 feat: add Safari-ready passkey login
Add passkey enrollment and login wiring, including a Safari-safe explicit login path that prefetches the WebAuthn challenge before the click and calls the credential ceremony immediately from the user gesture.
2026-06-01 03:09:17 +02:00
Matthew Meszaros 4663333d2c feat: update billing and email settings 2026-05-30 15:45:10 +00:00
Matthew Meszaros 2c713dfab3 feat: free-trial gets warmup + 1 inbox for 14 days
open CanUseWarmup and CanUseUnibox to free-trial orgs during the 14-day
window. add CanAddInbox feature gate and a FreeTrialInboxLimit of 1 so
trial orgs cannot seed the warmup pool with disposable accounts. enforce
the cap on both OAuthStart (avoids wasting the round-trip) and
OnboardSMTPIMAP, with distinct error messages for cap-reached vs
trial-expired.
2026-05-25 15:29:11 +00:00
Matthew Meszaros 337d823703 merge: bring main into branch, reconcile email service constructor 2026-05-24 04:09:16 +00:00
Matthew Meszaros 45f48ba93b feat: redesign email account onboarding flow 2026-05-24 04:01:16 +00:00
Matthew Meszaros abf73d168b fix: dropdowns + folder/tag create (server-side + client-side)
Two real bugs surfaced from "All folders / Newest dropdowns don't open"
and "hex color must be a valid string":

1) Dropdowns silently no-op (broken across the whole dashboard)
   PopoverMenuTrigger asChild uses React.cloneElement to inject
   onClick / ref / aria-expanded onto the trigger child. SelectButton
   was a plain function component that destructured a fixed prop set
   and rendered its own <button> — so the injected props were
   dropped on the floor. Click did nothing.

   Fix: SelectButton is now React.forwardRef + spreads {...rest} onto
   the inner button. The injected click handler reaches the real
   element, the dropdown opens, the menu renders, and selection
   actually applies state.

   Every PopoverMenu trigger using SelectButton was affected — that's
   campaigns (folders + sort), emails (tag filter), contacts (sort +
   filters page rows). All now work.

2) Adding a folder/tag failed with "hex color must be a valid string"
   The /folders + /tags POST landed on groupRepository.Create with
   an empty color and the validator rejected. Even before the color
   check, the INSERT used tx.QueryRow + Scan against an INSERT with
   no RETURNING clause, which always errored with
   "sql: no rows in result set" once it got past validation.

   API improvements (kept the design but made it forgiving):
   - Color defaults: if the request omits color, the server picks one
     from an 8-swatch palette based on the new item's position. Two
     consecutive creates won't end up identical. Non-empty but
     invalid still 400s — that's a client bug worth surfacing.
   - Title min length 3 → 1. "Q1", "VIP", short names are common
     and shouldn't fail. Trimmed before validation so " " doesn't
     pass.
   - INSERT now uses tx.Exec instead of QueryRow.Scan — the broken
     code would never reach success even when validation passed.

   Verified end-to-end:
     POST /folders {"title":"Q1"} → 200, color=#94a3b8 (default).
     POST /folders {"title":"Q2","color":"#38bdf8"} → 200.
     POST /tags    {"title":"VIP","color":"#10b981"} → 200.

   Frontend:
   - createFolder / createTag clients accept an optional color param.
   - LabelListModal now picks a default palette color when entering
     add-row mode (rotating with item count) and offers a swatch
     popover to override before submitting. Selected color is sent to
     the backend.
2026-05-23 09:18:45 +00:00
Matthew Meszaros d8d88c7f69 feat: add warmup health tracking, migrate repos to postgres, and overhaul web UI 2026-04-03 06:08:52 +00:00
Matthew Meszaros 6c6d26d8f0 Update auth and onboarding flow 2026-02-14 05:38:27 +01:00
Matthew Meszaros 772c19820d New Repository: Add Backend Code 2026-01-17 14:11:14 +00:00