Commit Graph
25 Commits
Author SHA1 Message Date
Gustavo 60d28c7465 Campanhas UI: try/catch on Marcar button, inline error for segmentos, cap variants at 3
- Wrap the "Marcar" stage button's onclick body in try/catch (toast on
  error), matching the pattern already applied to the 5 campaign action
  buttons.
- carregarSegmentos' error handler (from the produto <select> change
  handler) can fire while #modal-nova-campanha is open, where a toast
  alone is hidden behind the open <dialog> (documented in api.js next to
  the toast() function) -- now also surfaces the error in the existing
  inline #campanha-erro paragraph.
- addVarianteRow now caps at 3 rows (A/B/C, matching the convention used
  elsewhere in the UI/backend) and disables "+ Variante" once the cap is
  reached.
2026-08-03 13:07:13 -03:00
Gustavo 6f6ed97206 Add worker safety rails; split evaluateCampaignHealth into pure read + mutating apply
Two issues in the campaign worker path:

1. runCampaignQueue had none of the safety rails run.mjs's automatic sender
   has: no outreach.ativo gate, no business-hours window, no jitter between
   sends. A campaign left "rodando" would send around the clock even with
   outreach disabled or outside business hours, and would hammer the same
   WhatsApp number back-to-back. Added the same outreach.ativo/horaInicio/
   horaFim checks (duplicated, not imported, to keep the modules
   independent) plus a randomized 3-8s jitter between real send attempts.
   Sleep delay is injectable via an optional sleepMs param so tests can
   pass sleepMs: 0.

2. evaluateCampaignHealth mutated state (via applyPause) on every call,
   including from the read-only GET /api/campaigns/:id/saude route -- so
   simply opening the campaign detail panel in the UI could silently pause
   a healthy-looking campaign. Split it into a pure evaluateCampaignHealth
   (computes {pause, reason}, never writes) and a new applyCampaignHealth
   (evaluates, then persists the pause if warranted). The worker now calls
   applyCampaignHealth; the GET /saude route keeps calling the pure
   evaluateCampaignHealth.

Tests: worker sends nothing with outreach.ativo=false or outside business
hours; evaluateCampaignHealth confirmed not to mutate status even when it
would return pause:true; existing mutation assertions moved onto
applyCampaignHealth.
2026-08-03 13:07:03 -03:00
Gustavo 3a91ca4732 resumeCampaign resets semana_iniciada_em to avoid immediate escalation
resumeCampaign only updated status/pause_reason, so a campaign auto-paused
for many days and then resumed could immediately satisfy
escalateWeeklyLimits' 7-day check and double its daily limit right after
coming back from an unhealthy state -- the opposite of the intended
safety behavior. Now resumeCampaign also stamps semana_iniciada_em to
datetime('now'), resetting the weekly clock on every resume.
2026-08-03 13:06:51 -03:00
Gustavo aa46c3da07 Fix dead manual-send exclusion in campaign candidate query
The real manual-send route (routes/outreach.mjs) always inserts with
produto='' and lead_id=NULL, so the old single NOT EXISTS predicate
(produto = ? AND (lead_id = leads.id OR numero = leads.whatsapp)) could
never match a real manual send's row -- the numero branch was unreachable
in production even though a prior test fabricated a produto value manual
sends never write. Split into two NOT EXISTS clauses: one scoped to
produto+lead_id (automatic/campaign sends), one scoped to numero+
origem='manual' with no produto requirement (manual sends). Updated the
existing regression test to insert the row exactly like the real route
does (produto: '', lead_id: NULL, origem: 'manual').
2026-08-03 13:06:43 -03:00
Gustavo c7157ce96a Fix opt-out correctness: estagio=opt_out now blocks the number, numbers normalized to digits
Marking a lead opt_out via /estagio only wrote outreach_lead_states/
outreach_stage_events and never called addOptOut, so the lead stayed fully
eligible for future campaign sends. Also normalize numero with onlyDigits
in POST/DELETE opt-out routes and in the estagio lookup, matching how every
other number in the app (leads.whatsapp, queue.numero) is stored, so opt-out
comparisons never silently miss due to formatting differences.
2026-08-03 13:06:25 -03:00
Gustavo 5947a3eaa3 Add stage classification and appointment scheduling UI 2026-08-03 08:38:45 -03:00
Gustavo b4290452b3 Fix fila column label and add error handling to campaign action buttons
Relabel misleading "Empresa" header to "Lead ID" in fila table (queue
has no join to leads, cell showed lead_id). Wrap iniciar/pausar/retomar
and aprovar/cancelar selecionados handlers in try/catch with toast on
failure, matching btn-gerar-fila's existing pattern.
2026-08-03 08:35:56 -03:00
Gustavo 9aceaa140d Add campaign detail panel: queue approval, health and variant stats 2026-08-03 08:31:00 -03:00
Gustavo 5fef71d157 Fix segment picker misusing removable-tag .chip/.chips styling
The new-campaign segment checkboxes reused .chip/.chips, which is
reserved codebase-wide for click-to-remove selected tags (adds a
trailing " x" and removable-tag hover styling). Give the segment
picker its own segmento-opcao/segmento-lista classes with a minimal
non-removable style instead.
2026-08-03 08:27:35 -03:00
Gustavo e8745513e9 Add Campanhas tab: listing and campaign creation 2026-08-03 08:22:09 -03:00
Gustavo 0d360ad652 Add campaign HTTP routes 2026-08-03 08:17:36 -03:00
Gustavo 4835c6e085 Wire campaign scheduler into server boot 2026-08-03 08:13:45 -03:00
Gustavo a9509a4691 Add campaign send worker with daily limit and health auto-pause 2026-08-03 08:09:45 -03:00
Gustavo a2ecb135d4 Add campaign health auto-pause and variant comparison stats 2026-08-03 08:04:48 -03:00
Gustavo 9bf77d88d3 Fix candidatosEncontrados to reflect full eligible pool, not post-slice count
generateCampaignQueue computed candidatosEncontrados after slicing to
quantidade, so it could never differ from adicionadosNaFila. An operator
generating a queue with quantidade smaller than the eligible pool saw no
signal that more eligible leads existed. Now candidatosEncontrados is read
before the slice, while adicionadosNaFila still reflects rows actually
inserted.
2026-08-01 10:37:31 -03:00
Gustavo 248bd4c550 Add campaign candidate selection, queue generation and approval 2026-08-01 10:32:39 -03:00
Gustavo 8600893496 Add deterministic variant selection and message rendering 2026-08-01 10:29:19 -03:00
GustavoandClaude Sonnet 5 577f48ab19 Fix: stamp semana_iniciada_em when campaign first transitions to 'rodando'
Previously, setCampaignStatus did not set semana_iniciada_em, causing
escalateWeeklyLimits to bail early (if (!campaign.semana_iniciada_em) continue).
Now when a campaign transitions to 'rodando' for the first time, semana_iniciada_em
is set to datetime('now'). Subsequent rodando transitions leave it unchanged
(using COALESCE to preserve first-run timestamp).

Regression tests confirm:
- First transition to 'rodando' stamps semana_iniciada_em
- Subsequent transitions do not reset the timestamp
- resumeCampaign continues to work correctly (pause/resume cycle)
- All 105 tests pass

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-01 10:25:00 -03:00
Gustavo fb2cd75a56 Add campaign-store: CRUD, opt-out and weekly limit escalation 2026-08-01 10:19:22 -03:00
Gustavo a7abd3d4d0 Add campaign outreach schema (migration v8) 2026-08-01 10:14:54 -03:00
Gustavo 4f736562f3 Carry over pre-existing WABA outreach automation WIP
This work already existed uncommitted in the main working tree (not
authored by this branch) and is required as a runtime dependency for the
campaign outreach port (zpro-client, templates, outreach routes/UI). Copied
in as-is, untouched, so this feature branch has a working baseline; the
original uncommitted state in the main checkout is left exactly as it was.
2026-08-01 10:12:25 -03:00
Gustavo 0894ab68b3 Ignore .worktrees/ directory 2026-08-01 10:09:32 -03:00
Gustavo 3ce12381d5 Add implementation plan for campaign outreach subsystem port
11-task TDD plan covering schema migration, campaign store, candidate
selection/queue, health metrics, send worker, scheduler, HTTP routes and
the Campanhas UI tab.
2026-08-01 10:08:07 -03:00
Gustavo 2195b3f2b1 Add design doc for campaign outreach subsystem port
Ports the Máquina Comercial Pessoal's campaign methodology (queue/preview/
approval, A/B/C variants, weekly limit escalation, health-based auto-pause,
stage classification, appointment scheduling), adapted for the Z-PRO/WABA
send layer and a dynamic operator-defined catalog instead of a hardcoded one.
2026-08-01 09:47:49 -03:00
DESKTOP-F2O4A80\UserandClaude b8d10b210c Commit inicial: GTI Maquina Comercial
App local (Node + SQLite) para captura, tratamento e exportacao de leads
comerciais, com dashboard de gestao, login/usuarios e integracao Z-PRO.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-12 14:22:21 -03:00