Commit Graph
83 Commits
Author SHA1 Message Date
Matthew Meszaros efb9d0b433 feat: add the qa/ proof harness that records scripted Playwright flows as 1080p H.264 walkthroughs and stills against an isolated per-worktree stack (lite, full, sandbox) and publishes them to the PR with gh --attach, with before/after follow-up comments, a machine-wide recording lock, idle auto-stop, a qa-ci typecheck job and AGENTS.md rules for when to record 2026-10-03 09:54:59 -07:00
Matthew Meszaros 4183929b29 feat: note in AGENTS.md that useClickOutside also closes on a tap landing in a same-origin iframe 2026-10-01 22:32:51 -07:00
Matthew Meszaros 76f43b9796 feat: close every dashboard dropdown through one useClickOutside(open, onClose, refs) hook that the shared PopoverMenu, the old useClickOutside callers and the hand-rolled pickers now all use, so a press outside (including inside a floating dialog card or an email iframe) closes it, Escape closes only the innermost one and returns focus to its trigger, and opening one closes the others 2026-10-01 08:38:57 -07:00
Matthew Meszaros 57a26c4ee6 feat: attribute a received warmup email moved to spam on the evidence around it before charging anyone, never for mail that arrived in spam (warmup_received.landed_spam), holding each move 30 minutes in warmup_spam_moves and deciding provider on a cross-workspace correlation or a move straight after arrival with nobody there, owner on provider-reported read, unread or star activity in mailbox_owner_activity or a repeated uncorrelated pattern in a used mailbox, nobody otherwise, weighting a spam move as one strike, withdrawing owner verdicts a later correlation explains, clearing the strikes behind a hold an admin lifts or an appeal approves, migration 000233, docs and guide 2026-09-30 06:52:32 +02:00
Matthew Meszaros ad4104c57a feat: never add a warmup strike from a recheck (confirm it, or withdraw it when the message is still in the mailbox or retention removed it since), revise a tampering hold before its strike is deleted so a retry completes it, compare-and-swap the revision on the hold's term, scope the ledger revision to the whole address, keep tampering events 37 days so a live hold's strikes survive pruning, answer an inconclusive search as unknown so IMAP rechecks stop, and return an IMAP transport failure as an error 2026-09-29 05:12:36 -07:00
Matthew Meszaros e5eb3b0ff1 feat: re-decide a withdrawn warmup tampering hold on the strikes left in the seven days before it was imposed (on the pool row, or the reputation ledger for a mailbox out of every pool), stamp an old strike verified only when a worker answers its search and ask again after six hours, retry a removal check for a mailbox still loading, redeliver a failed strike, search IMAP folders in one session hold, and share the Message-ID search helpers 2026-09-29 04:57:45 -07:00
Matthew Meszaros c5a981d86c feat: record a warmup deletion strike only after the worker searches the mailbox and finds the message in the trash or gone, so a move reported as a removal (Graph, a provider filter, a mailbox rule, a second Warmbly instance, our own filing) is never charged, withdraw strikes whose message is still in the mailbox and lift the pause or block they imposed, recheck deletion strikes recorded before the search once via warmup_tampering_events.verified_at (000229), and clarify the tampering reasons and warmup guide 2026-09-29 04:28:34 -07:00
Matthew Meszaros 25075fcdd8 feat: take the warmup inbox rate and its daily rolling line at Google, Microsoft and Yahoo only (small hosts ride beside it and never make the headline), and rebuild the Accounts mailbox list in the Leads style with host logos, sender and connection line, a status naming whether the mailbox is sending, warming or both, sortable Sent today, animated Warmup, Inbox and pulsing Health columns, with docs and OpenAPI updated 2026-09-27 20:40:45 -07:00
Matthew Meszaros 2b1a35dfe1 feat: make warmup spam placement only slow a mailbox down (watch at 10%, half-volume throttle at 20%, never quarantine, block or appeal) and judge it only at Google, Microsoft and Yahoo over verified deliveries, keep small-host spam out of the health bands, warmup and cold ramps and the advisor while still showing it beside the headline rate on the drawer and Deliverability, draw small-host partners whose own filter junks warmup mail less often, send each quarantine or block webhook once plus health_changed, release placement-only quarantines in 000220, and update the warmup, deliverability, advisor and API docs 2026-09-26 22:26:58 -07:00
Matthew Meszaros af8b08d27a feat: rank borrowed free warmup mailboxes strictly by provider then tenure then activity, build the two inbound-share candidate suffixes once, and correct the partner-limit wording in the drawer, OpenAPI, warmup and Warmbly Cloud docs and AGENTS.md 2026-09-25 21:52:45 -07:00
Matthew Meszaros 6e62c500c3 feat: borrow the best proven free warmup mailboxes whenever a premium mailbox has fewer outside-workspace partners than max(25, its warmup ceiling) so siblings no longer block borrowing, keep a quarter of every inbox's daily warmup capacity for premium senders, and show the pool and any partner cap on today's target in the mailbox drawer 2026-09-25 21:47:36 -07:00
Matthew Meszaros b2d54fc873 feat: record where every warmup email lands (inbox, Gmail tab, spam) per sender, day and recipient host in warmup_placement_daily, serve it from GET /analytics/warmup/placement, cap mailbox health at the measured 7-day inbox rate, and show it as an Inbox column, a mailbox Deliverability tab and a workspace placement section; replace every visible native checkbox in the dashboard with a themed Checkbox and make the mailbox drawer's tab bar scroll 2026-09-24 05:28:51 -07:00
Matthew Meszaros eac3f6d615 Merge remote-tracking branch 'origin/main' into fix/sso-link-existing-password-account
# Conflicts:
#	docs/content/docs/guides/security.mdx
2026-09-21 04:28:20 -07:00
Matthew Meszaros 9426c0da51 feat: validate every person, workspace and company name through internal/pkg/displayname on each write path (profile, onboarding, setup, IdP sign-in, org create and rename, org import, enterprise inquiry, admin testers, warmblyctl) with a 400 invalid_name code, render stored names in platform email through the same rules, mirror them in the web forms, check the org slug format, and document the rules in error-codes, security and AGENTS.md 2026-09-21 03:34:03 -07:00
Matthew Meszaros 0f60fd9b84 feat: attach a Google, Apple or OIDC identity to an existing password account only after that account's password is presented: resolveFederatedUser parks the sign-in as link_required with a single-use sso_link pending token, POST /auth/sso/link checks the password against the provider-asserted address on the sign-in failure budget and links then issues the session through finishLoginAs, the dashboard collects it on a new login step, and the API reference, endpoints list, security guide and OpenAPI spec describe the third login result 2026-09-21 03:25:29 -07:00
Matthew Meszaros 1513419a2a feat: delete warmup mail from each mailbox once past a per-mailbox retention window (email_accounts.warmup_retention_days, else retention.warmup_mail_days, default 30) via a consumer sweep that retires the receipt and sender copy and a worker delete action that trashes on Gmail, deletes on Graph, expunges on IMAP and drops the stored body, prune per-message warmup records after retention.warmup_event_days, and count a warmup deletion as tampering only within 24 hours of arrival and never for a retired message, judging Gmail's Trash label on the same rule 2026-09-21 00:52:02 -07:00
Matthew Meszaros 26594391c8 feat: judge tampering with received warmup mail on a ladder inside the health bands, one deletion warns, two pause for seven days and four or two spam flags block for thirty, instead of a review-required block on the first deletion (#635) 2026-09-20 09:50:42 -07:00
Matthew Meszaros d6384d3c0e feat: make cross-tier warmup an exchange so a proven free mailbox writes back to the paying mailboxes that wrote to it, favour the inbox owed the most on every draw, cap what any inbox receives per day inside WarmupPartnerCandidates so a thin tier is neither starved nor flooded, and surface received counts in the mailbox drawer, warmup analytics and the API (#633) 2026-09-20 09:42:53 -07:00
Matthew Meszaros 28b3dc9f05 feat: move the CASA evidence pack out of this repository to CASA_EVIDENCE_DIR and make the generator refuse any destination inside the tree, because a pack that maps every control to its file and lists the advisories still open with their reachability conditions is a reconnaissance document for anyone attacking a self-hosted instance that has not updated yet 2026-09-19 13:08:33 +02:00
Matthew Meszaros acecd62c88 feat: scope mailbox disconnect and warmup lifecycle to the workspace rather than the member who connected the mailbox so an admin can act on every mailbox the list already shows them, evict a mailbox whose row is gone from every live worker when its provider errors arrive so a deleted mailbox stops calling the provider once a sync interval forever, subscribe before publishing the credential-validation job and classify a socket deadline as the retryable timeout it is, give the worker's validation reply its own budget so a slow mail host no longer loses a finished verdict, guard every global key handler against a keydown carrying no key, drop exceptions whose whole message is an object's default toString, make the Postgres pool size configurable, and record the CASA and security invariants in AGENTS.md 2026-09-19 12:49:46 +02:00
Matthew Meszaros 5072f27b59 feat: correct the AGENTS.md codec note that still claimed Avro cannot serialize the worker command and result envelopes, which the derived union schemas in internal/models/event_schema.go made false 2026-09-16 19:37:32 +02: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 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 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 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 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 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
Matthew Meszaros 2998f8a8c6 Merge remote-tracking branch 'origin/main' into feat/worker-capacity-soft-target 2026-09-10 06:26:47 -07:00
Matthew Meszaros fa2b5330d7 feat: drop the auth-pressure placement term because worker_capacity_view aggregates auth_errors (per-mailbox credential failures) and not rate_limit_errors (the 454/421 per-IP throttles it claimed to measure), measure projected utilization against an age-free Capacity.Target so a freshly joined node can relieve a full fleet instead of scoring as 200% loaded after one mailbox, bound the isolated-egress override with an explicit OverTarget check now that Eligible no longer caps it, and cap rotation moves per destination since a tick scores every mailbox against one frozen materialized-view snapshot 2026-09-10 05:51:17 -07:00
Matthew Meszaros 7fa4fdfbc4 feat: make worker capacity a placement target rather than a hard gate, so Eligible refuses only on health and an over-target worker costs enough score to lose to anything with room instead of returning nil and dropping assignment into selectFallback, score projected utilization including the incoming mailbox's own weight, and penalise the 454/421 auth pressure the capacity view already collected and threw away 2026-09-10 05:38:17 -07:00
Matthew Meszaros 510ee692ba feat: address the review on the split-deployment branch by moving the two broker routes onto their own NODE_BROKER_TOKEN so the internet-facing tracking and forms services no longer hold a credential that can open any organization's data key, refusing to presign any key outside the prefixes a node reaches, fixing IAM policies that named an alias ARN KMS never resolves in a Resource element, bounding both brokered HTTP clients because the sync loop's context never expires, no longer reporting a 403 from the object store as a missing body, and redacting the DSN and URL credentials the dry-run listing printed in clear 2026-09-10 14:19:53 +02:00
Matthew Meszaros 47ba13083e feat: make a split deployment work end to end by fixing the three defects that made an off-host node impossible to configure (nodeEnvKeys shipped S3_BUCKET and KMS_KEY_ID, which nothing reads, so an AWS-backed node silently used the default bucket and key alias; a joined consumer never received PRIMARY_DB and died at boot; and node.env was rewritten on every join with no file an operator could add to), then removing the need for cloud credentials on a node at all with brokered KMS and blob providers that renderNodeEnv hands out automatically, plus deploy/split-cloud, scripts/aws-bootstrap.sh, two fleet instance checks and the docs 2026-09-10 13:58:59 +02:00
Matthew Meszaros a5993a1ed0 feat: put the standalone-statement note where the docs said it was, shellcheck the join-script checker as well since its own disable directives are load-bearing, and stop claiming the EnvironmentFile assertion covers two render variants when only the mount list varies with the environment 2026-09-09 07:04:58 -07:00
Matthew Meszaros 9203695a41 feat: harden the join-script guard against being fooled rather than against being reformatted, matching the ensure_blob_root call on its first field after a looser regex proved satisfiable by the name appearing inside a warn string, capturing the function body without a pipeline so a renamed function reports that instead of the assertion it happened to fail, and rendering both unit variants so the no-blob one is covered too 2026-09-09 06:59:14 -07:00
Matthew Meszaros eb4e1a8bb8 feat: make the join-script checker survive its own mutation tests, matching the ensure_blob_root call on the command field so a commented-out call no longer passes, tolerating a space before the parentheses in a function definition, and capturing the function body before asserting so a renamed function reports that rather than the assertion it happened to fail 2026-09-09 06:47:56 -07:00
Matthew Meszaros 6183e8b238 feat: close the join-script checker's blind spots by pinning the image reference outside the node-writable mount, asserting at their call sites the two invariants that leave no trace in the rendered unit, and matching call lines rather than any line mentioning the word so a comment containing enrolment cannot satisfy the ordering check 2026-09-09 06:42:56 -07:00
Matthew Meszaros 6947aca9b3 feat: make the join-script check assert on what the script renders via a new --print-unit mode, after the previous version compared a heredoc copied into the checker and stayed green when the systemd command-substitution bug was put back, and make docker_mounts pure so the unit can be rendered without creating directories 2026-09-09 06:37:56 -07:00
Matthew Meszaros 29bf9b4320 feat: cover the join script with make join-check, wired into make lint, since nothing tested the highest-consequence non-Go file in the repo and three defects reached the branch through it; and fix the four the review found in the last round, matching the fs provider alias, creating blob parents under a 0022 umask rather than the 0700 one write_config leaves set, warning instead of silently mounting a blob root the node cannot write, and validating the path right after enrolment rather than halfway through the install 2026-09-09 06:28:30 -07:00
Matthew Meszaros 435dbb522f feat: replace the worker tier/type/risk-pool/egress categories with a scored placement model and make the fleet pull-based, so a machine joins with one command, workers and consumers share one node registry with usage and liveness, nodes self-update to the version the control plane resolves, and the Hetzner provisioning, worker profiles and SSH orchestrator are removed 2026-09-09 04:54:01 -07:00
Matthew Meszaros ed50ad9f2f feat: add a campaign entry delay so a contact's first email can wait a set time after they enter the campaign, with campaigns.entry_delay_minutes and a campaign_leads.added_at anchor (migration 000136), the delay applied in the router's per-lead due check and floored into the placer through ContactSequencePair.NotBefore, a distinct entry_delay constraint in the contact next-action preview, and the control surfaced on the Schedule tab, a new Trigger card at the top of the Steps canvas, the campaign wizard's Schedule step, the launch dialog, the update_campaign AI tool and the iOS schedule page, plus guides, API reference and live scheduler and repository tests 2026-09-08 05:06:04 -07:00
Matthew Meszaros 23f88293c8 feat: document the warmbly CLI at docs.warmbly.com/api/cli with every install channel, the device flow in api/authentication, the new auth/cli and api-keys/self routes in the endpoint scope map, a warmbly-cli agent skill, and a note on warmblyctl saying which of the two CLIs a reader wants 2026-09-04 20:14:43 -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 84815381a9 fix: route the orphaned website tracking settings page, which the settings nav linked to but main.tsx never registered, and give every unmapped route a document title so forms, segments, categories, oauth apps and six settings pages stop rendering the literal Page not found in the browser tab 2026-09-01 09:26:40 -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 d7a17a0149 feat: make the per-mailbox daily campaign cap configurable up to 5000 (issue #276): raise campaign_limit, campaign daily_limit and ramp start/ceiling validation to config.LimitMax, warn in the dashboard above 100/day, and update aitools, zapier and docs copy to match 2026-08-31 03:50:36 -07:00
Matthew Meszaros 15e139e15d feat: stop a campaign email going out twice when the progress write after dispatch is lost: a step is now RESERVED before its SEND_EMAIL reaches the bus (migration 000093 adds campaign_contact_progress.dispatched_at + dispatch_task_id, and ReserveSend takes the claim and the day's counters in one transaction) and routing treats a step as attempted on sent_at OR dispatched_at, so a crash or a failed stamp in the dispatch window can no longer read as "never sent" and email the same person again; the ON CONFLICT claim is exactly-once so two ticks racing the same pair cannot both send (the loser ends skipped_duplicate), the stamp is retried and escalated to the campaign feed instead of warned and swallowed, HandleEmailSent repairs a lost stamp from the worker's own confirmation, ReleaseSend gives a reservation back only when the command provably never left (a publish failure is ambiguous via ErrSendDispatchUnknown and keeps it), and StartStuckSendReclaimer walks back a reservation nobody answered after 30 minutes so a worker that died mid-send cannot park a lead in flight forever; live-tested in TestLiveLostProgressWriteDoesNotResend, TestLiveDispatchedSendIsNeverOfferedTwice, TestLiveConcurrentTicksSendOnce, TestLiveStuckDispatchIsReclaimed, TestLiveReclaimBelievesADeliveredSend and TestLiveInFlightSendIsNotOfferedAgain 2026-08-24 09:15:06 -07:00