Commit Graph

37 Commits

Author SHA1 Message Date
Matthew Meszaros 2170b86106 fix: stop import 500 by installing no-op audit service and drop redundant banner 2026-05-25 06:19:22 +00:00
Matthew Meszaros 6eb3a3bdfd merge: bring origin/main into branch, layer full seed on top of seedRich 2026-05-24 11:54:49 +00:00
Matthew Meszaros 0c1fa08a35 feat: full dev seed (users, orgs, workers, mailboxes, campaigns, crm, admin) 2026-05-24 11:41:51 +00:00
Matthew Meszaros 939f2160d9 Merge pull request #6 from warmbly/feature/redesign-email-onboarding
ci: fix CI permissions, add web pipeline, prune go linters
2026-05-24 13:26:25 +02:00
Matthew Meszaros c822e95e7f Merge remote-tracking branch 'origin/main' into feature/danger-zone-delayed-deletions
# Conflicts:
#	cmd/backend/main.go
#	internal/api/handler/handler.go
#	internal/api/routes.go
#	internal/models/audit.go
#	internal/models/organization.go
#	internal/models/user.go
#	internal/repository/pg_organization.go
#	internal/repository/pg_user.go
2026-05-24 04:12:10 +00:00
Matthew Meszaros 337d823703 merge: bring main into branch, reconcile email service constructor 2026-05-24 04:09:16 +00:00
Matthew Meszaros e42feac0e4 feat: danger zone with delayed deletion for orgs and accounts 2026-05-24 04:06:07 +00:00
Matthew Meszaros 45f48ba93b feat: redesign email account onboarding flow 2026-05-24 04:01:16 +00:00
Matthew Meszaros d227038ca0 ci: drop unused/unconvert/gosimple + shadow/nilness, run gofmt
Disable the linters that fire on legacy code without flagging real
bugs: `unused` (orphan repos kept for future feature flags),
`unconvert` (defensive type conversions), `gosimple` (style
suggestions in code we don't want to touch).

govet: disable `shadow` (idiomatic `err :=` re-decls in transaction
patterns) and `nilness` (legitimate defensive nil checks that look
tautological to the analyzer).

Ran `gofmt -w internal/ cmd/` — every Go file now passes
gofmt -l with no output.

Kept: govet, staticcheck, ineffassign, typecheck, bodyclose, noctx,
sqlclosecheck, gofmt, goimports, misspell — the real-bug checks.
2026-05-23 16:54:12 +00:00
Matthew Meszaros 4eb8f7babe feat: settings overhaul, avatars, RBAC, plan alignment, perf
Backend:
- Fix contact-create 500 (nil custom_fields, doubled slice, bad RETURNING SQL)
- Avatar upload: migration 000033, S3 public-read, PNG/JPG only,
  client-resized to 512px + server dimension cap (1024px)
- Pull avatar_url through user + organization repo queries

Frontend:
- Settings restructured into nested routes with a rail layout
  (/app/settings/{profile,notifications,security,members,roles,
  workspace,billing,danger}); flat Section/Row primitives replace
  the per-card rectangles; Save buttons only render when dirty
- Standalone /app/billing and /app/team removed; legacy URLs
  redirect to the settings sections; UserNav trimmed accordingly
- CRM rebuilt: Pipelines CRUD + stage editor, Deals kanban with
  HTML5 drag/drop, Tasks bucketed by due-date with inline toggle.
  Frontend models realigned with backend (Deal.name, CRMTask.status
  enum, paginated list shapes)
- Avatars: AvatarUploader component, client-side canvas resize,
  wired into Profile + Workspace settings; UserNav + OrgSwitcher
  render the uploaded image with initials fallback
- RBAC: lib/permissions.ts mirrors organization_permission.go;
  inline role picker in Members; Roles & access section shows the
  permission matrix and per-role member counts
- Audit log page at /app/audit, gated to owner+admin via canManage
- Plans aligned with warmbly-web pricing: Starter/Grow/Business/
  Enterprise via lib/plans.ts; PlanPill, billing page, sidebar
  badges and LockedSurface all read from the same catalogue
- Header PlanPill shows current plan with status-aware coloring;
  sidebar locked rows show the required-plan badge instead of a
  generic lock icon

Perf:
- QueryClient defaults (staleTime: 30s, refetchOnWindowFocus: false,
  retry: 1) — kills 3-5 round-trip storm on every navigation
- useSubscription, usePlans → staleTime: Infinity (only invalidate
  on plan-change mutations); useUser/Timezones/Orgs get long stales
  with refetchOnMount: false
- vite.config: optimizeDeps for heavy libs + server.warmup for the
  most-mounted entry pages
2026-05-23 16:04:24 +00:00
Matthew Meszaros 7e02bb2a5a feat(consumer): hourly risk rebalancer migrates mailboxes between risk pools
New background job in the consumer process:

  1. Pulls up to 1000 mailbox candidates joined with their worst warmup
     health state (across all pools they participate in) and their
     current worker's risk_pool. Dedicated workers are excluded — single
     tenant, segregation not applicable.
  2. Recomputes risk_band from health state via RiskBandFromHealth.
     If it changed, writes the new band.
  3. If the band's matching pool doesn't equal the worker's pool, picks
     a new worker via SelectSharedWorkerForBand and migrates the mailbox.
     Increments/decrements account counts.
  4. Logs each migration to admin_audit_log with action=
     "risk_rebalance_migrate" so operators see what moved and why.

Boot-time run + hourly ticker. Rebalancing is intentionally batch, not
event-driven: warmup health states change on a slow rolling-window basis
(warmup_health_sweep is also hourly), so reacting in real time gains
nothing and would cause thundering-herd migrations.

JobsService gets an AssignmentService dep. Nil disables the job (lets
self-hosters opt out by simply not wiring it).
2026-05-19 05:39:27 +00:00
Matthew Meszaros 2aefb7da02 feat(consumer): log auto-reassignment events to admin_audit_log
When the dead-worker job reassigns email accounts from a worker whose
heartbeat expired, write a row into admin_audit_log so the dashboard's
audit viewer surfaces these system actions alongside admin-driven ones.

admin_user_id is uuid.Nil (the platform identity), so admins searching
the log can distinguish "system did this" from "an admin did this" by
filtering on that ID. Details include the replacement worker, account
count, and reason.

JobsService gets an optional AdminRepo dep. Nil disables logging — keeps
the contract loose for any other call site that doesn't have one.
2026-05-19 05:21:26 +00:00
Matthew Meszaros aaa77eecb2 test(seed): cover seedBaseline + seedRich for shape and idempotency
The seeder is one of the few things every developer runs on every new
checkout, but it had zero tests. With three migrations added in the last
few days and the rich-fixture path now creating 30+ rows, the chance of
silently breaking a schema migration without noticing was non-trivial.

cmd/seed/main_test.go connects to SEED_TEST_DB (skips otherwise — keeps
unit tests in CI fast and prevents accidentally clobbering a dev
database), runs migrations, wipes only the fixture rows, then:

  1. Runs seedBaseline twice, verifies row count stays at 1.
  2. Runs seedRich, asserts 9 different row counts match expectations
     (users, orgs, workers, accounts, campaign, sequences, contacts,
     unsubscribed contacts, campaign leads).
  3. Re-runs seedRich, asserts every count is unchanged — the most
     important guarantee the seeder makes.
  4. Verifies warmup pool membership: 2 free, 4 premium, with the
     correct accounts in each.

Plain testing package, table-driven, matches the existing style in
internal/app/warmup/service_test.go.

`make test-seed` brings up the docker-compose Postgres and runs the
suite against it.
2026-05-18 14:56:06 +00:00
Matthew Meszaros 8ae7759964 feat(consumer): sync worker heartbeats from Redis to workers.last_seen_at
Workers heartbeat into Redis every 90s as RFC3339 timestamp values with a
3-min TTL. The dashboard surfaces liveness based on workers.last_seen_at,
but until now nothing populated that column — the "Live" badge was always
red.

New 60s job in the consumer reads each active worker's Redis heartbeat
value, parses the timestamp, and writes it to workers.last_seen_at. Runs
on its own interval (separate from the 5-min dead-worker detection job,
which does heavier reassignment work) so the UI sees fresh data within a
minute.
2026-05-18 14:55:34 +00:00
Matthew Meszaros 261cc439ad feat(admin): manage worker fleet from dashboard with encrypted credentials and GitHub release auto-update
Workers are no longer curl|sh-only. Admins add and manage them from the
dashboard over SSH, with all runtime config (Kafka, Schema Registry,
Redis, AWS keys) stored encrypted via the existing KMS-envelope cipher
service.

Worker lifecycle:
  1. Admin POSTs host/port/user. Backend generates an ed25519 keypair,
     encrypts the private key under uuid.Nil (platform identity), and
     stores the row in 'pending' state.
  2. Admin pastes the returned public key into the VPS's authorized_keys.
  3. Test connection — runs `true` over SSH, pins the host SHA256
     fingerprint on first success (TOFU).
  4. Install — backend scp's install-worker.sh + a per-worker env file
     and runs it. State moves pending → provisioning → installed.
  5. From then on: restart, update image, apply config, uninstall,
     rotate keys, tail logs, live status, OS package update, reboot —
     all dashboard buttons backed by SSH operations.

Credentials are reusable entities:
  - aws_credentials: named keypair, secret encrypted at rest
  - worker_profiles: bundles Kafka + Schema Registry + Redis + image +
    release channel, references one AWS credentials row
  - workers.profile_id links a worker to a profile; many workers can
    share one profile

Saving a profile doesn't restart anything. The dashboard compares
profile.updated_at to each worker's config_applied_at and shows a
"stale config" badge; Apply rewrites /etc/warmbly/worker.env over SSH
and restarts the unit.

Auto-update on GitHub release:
  - profile.release_channel ∈ {pinned, stable, dev}
  - profile.auto_update toggles automatic rollout
  - Trigger model is push, not poll: one check on backend boot, then
    the /webhooks/github/releases endpoint (HMAC-validated with
    RELEASES_WEBHOOK_SECRET) on every release event. Manual "Check now"
    button as fallback.
  - When a new tag resolves, the orchestrator SSHes into each assigned
    worker, runs install-worker.sh --update --image <new>, which now
    rewrites the systemd unit (not just `docker pull`) so the image
    actually changes. workers.image_version captures the running tag
    for the UI's "v1.2.3 → v1.2.4" diff.

Self-hostable: every release knob is env-driven —
RELEASES_GITHUB_REPO, RELEASES_WORKER_IMAGE_REPO,
RELEASES_WEBHOOK_SECRET, RELEASES_GITHUB_TOKEN, RELEASES_ENABLED. Set
RELEASES_ENABLED=false to disable the feature entirely.

OS-level updates and reboot are also exposed: detect apt / dnf / yum /
pacman / apk, run the right upgrade noninteractively, return the full
output and a reboot-required flag. Reboots are never automatic.

Migrations:
  000028_worker_ssh        — ssh fields, install_state enum, last_seen,
                              host fingerprint
  000029_worker_credentials — aws_credentials + worker_profiles +
                              workers.profile_id + workers.config_applied_at
  000030_worker_releases   — release_channel enum, auto_update,
                              resolved_image_tag, workers.image_version

Endpoints added:
  POST   /admin/workers                        (create + keypair)
  GET    /admin/workers/managed
  GET    /admin/workers/:id/managed
  POST   /admin/workers/:id/{test,install,restart,upgrade,uninstall,rotate-keys,apply,system-update,reboot}
  PUT    /admin/workers/:id/profile
  GET    /admin/workers/:id/{live-status,logs}
  DELETE /admin/workers/:id
  GET    /admin/aws-credentials                CRUD
  GET    /admin/worker-profiles                CRUD + /workers + /apply + /release
  GET    /admin/releases/state
  POST   /admin/releases/check
  POST   /webhooks/github/releases             public, HMAC-validated

Admin UI:
  /app/admin/workers           list with status + version columns
  /app/admin/workers/new       add form with profile dropdown
  /app/admin/workers/:id       detail with all actions + logs + system update
  /app/admin/credentials       tabs: AWS credentials + worker profiles,
                                Releases panel, channel selector +
                                auto-update toggle in profile form
2026-05-18 13:09:11 +00:00
Matthew Meszaros d25eed3eb6 feat(dev): root docker-compose with profiles, LocalStack, richer seed
Hoist the dev/sim stack to a single docker-compose.yml at the repo root.
Adds profiles (default / sim / seed / tools) so you can opt into heavier
setups, and bundles dependencies that were previously missing:

- LocalStack (KMS + DynamoDB + S3) with a localstack-init one-shot that
  idempotently creates alias/master-key-dev, the UserEncryptedKeys and
  EmailMessageData tables, and the main S3 bucket. Backend and workers
  wait on it via service_completed_successfully.
- stripe-mock for billing flows
- kafka-ui under the tools profile

Three workers with deterministic UUIDv5 hostnames (shared / premium /
dedicated) so assignment, rebalancing, and per-pool routing all have
real targets to exercise.

Richer seed (cmd/seed/main.go) loads 3 orgs across tiers, 6 mailboxes
joined to free/premium warmup pools, a Beta campaign with a 2-step
sequence, and 10 contacts (2 unsubscribed) so suppression behaviour is
visible in the UI. Idempotent — safe to re-run.

Makefile targets:
  make dev    — infra + app + one worker
  make sim    — adds premium + dedicated workers
  make seed   — rich fixtures
  make tools  — kafka-ui at :18090
  make reset  — nuke volumes
2026-05-18 13:08:34 +00:00
Matthew Meszaros 1ad8e7e995 feat: implement dead worker detection with heartbeat TTL and auto-reassignment 2026-04-12 11:14:24 +00:00
Matthew Meszaros a741b4939a feat: implement worker HandleAddEmail and HandleRemoveEmail handlers 2026-04-10 06:56:35 +00:00
Matthew Meszaros 7e123a2dac fix: remove deep health endpoint and add IP rate limiting to public endpoints 2026-04-09 16:03:37 +00:00
Matthew Meszaros 23b5c925e4 feat: add scheduled warmup health sweep, pool health summary endpoint, and admin overview 2026-04-09 15:57:04 +00:00
Matthew Meszaros 816c432e71 feat: add deep health check endpoint with PostgreSQL connectivity check 2026-04-09 15:49:28 +00:00
Matthew Meszaros 0799020dac feat: add metrics, warmup content variety, tz-aware scheduling, org budget, admin stubs, and bug fixes 2026-04-09 14:33:45 +00:00
Matthew Meszaros 19d20e72b4 feat: add warmup health throttled state, complaint/bounce metrics, A/B analysis, and rate limiting 2026-04-09 12:51:30 +00:00
Matthew Meszaros 308f72b874 feat: add CORS origin config, encrypt email bodies in S3, replace printfs with structured logging 2026-04-03 06:24:20 +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 21ffb6a748 feat: add advanced outreach controls with A/B testing, deliverability dashboard, and DLQ 2026-02-20 08:59:17 +00:00
Matthew Meszaros c564b3ac95 feat: implement unibox replies, warmup conversations, and daily email limits 2026-02-20 04:54:46 +00:00
Matthew Meszaros c12fd93def Add local Sentry logging 2026-02-14 05:49:49 +01:00
Matthew Meszaros 6c6d26d8f0 Update auth and onboarding flow 2026-02-14 05:38:27 +01:00
Matthew Meszaros 2635e9c6da frontend & email design; local email setup 2026-02-12 17:25:46 +01:00
Matthew Meszaros 141bc54974 Add sample auth UI theme 2026-02-10 19:30:47 +01:00
Máté Mészáros (Laptop) 8e3c399232 Auto deploy on new release 2026-01-30 04:29:07 +01:00
Máté Mészáros (Laptop) 7ed5719c15 Config & Deploy 2026-01-29 08:50:38 +01:00
Máté Mészáros (Laptop) 41624a6f79 Analytics & Tracking 2026-01-29 05:59:04 +01:00
Máté Mészáros (Laptop) 6adb4cdd5a Organization, Subscription, Inqueries, limits. 2026-01-27 05:55:48 +01:00
Máté Mészáros (Laptop) ea787554ae Oidc Middleware & Tasks Service 2026-01-19 15:54:57 +01:00
Matthew Meszaros 772c19820d New Repository: Add Backend Code 2026-01-17 14:11:14 +00:00