Files
GustavoandClaude Sonnet 5 417da56bd6 chore: import maquina-comercial-pessoal history via subtree
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

git-subtree-dir: maquina-comercial-pessoal
git-subtree-mainline: f5af038e19
git-subtree-split: a9ef4d776d
2026-09-28 15:50:17 -03:00

27 KiB

Commercial Outreach Pilot Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a supervised, measurable WhatsApp prospecting operation for GTI Vendas and G-Delivery with approved daily queues, gradual number verification, inbound-reply pauses, a manual commercial funnel, and conservative scheduling.

Architecture: Add a campaign layer that references existing leads, verification, opt-out, and provider-send records without duplicating their authority. Queue preparation, scheduling, execution, inbound handling, pipeline state, metrics, and UI each live in focused modules with transactional boundaries at approval, reservation, reply, and stage changes.

Tech Stack: Node.js 24 ESM, node:test, SQLite DatabaseSync, Baileys 7.0.0-rc14, browser JavaScript, Playwright browser acceptance tests.

Global Constraints

  • Work only in MAQUINA COMERCIAL PESSOAL.
  • Products are exactly GTI Vendas and G-Delivery.
  • The sender is Gustavo - Representante Comercial do Grupo GTI.
  • Do not put price, links, CNPJ, source-database details, unsupported features, unapproved integrations, technical deadlines, or guaranteed results in the initial message.
  • Every initial message contains a simple refusal mechanism.
  • The pilot has no follow-up.
  • Campaign order is G-Delivery food delivery, GTI Vendas food/markets, then GTI Vendas retail/services.
  • Operating windows are Monday-Friday 09:30-11:30 and 14:00-17:00 local time.
  • Daily campaign progression is 10, 12, 15, and 18; weekly increases require explicit approval.
  • Campaign sends use random 12-25 minute intervals and at most four attempts in a rolling hour.
  • Manual and automatic daily limits are separate and displayed separately.
  • Previously unverified checks per day are capped at ceil(1.5 * approved campaign daily limit).
  • Any inbound message pauses the equivalent phone/lead; no automatic commercial reply is sent.
  • Unknown delivery is terminal and never retried automatically.
  • Automation starts disabled after every server restart.
  • Do not send real messages from automated tests.
  • Do not create a Git commit unless the user explicitly requests one.

Task 1: Campaign Persistence and Fixed Catalog

Files:

  • Modify: src/db.mjs
  • Create: src/outreach/campaign-store.mjs
  • Create: src/outreach/campaign-catalog.mjs
  • Test: tests/campaign-store.test.mjs
  • Modify: tests/db.test.mjs
  • Modify: tests/personal-branding.test.mjs

Interfaces:

  • Produces: CAMPAIGN_PRODUCTS, CAMPAIGN_STAGES, CAMPAIGN_DEFINITIONS

  • Produces: seedCampaigns(db), listCampaigns(db), getCampaign(db, id)

  • Produces: getLeadCommercialState(db, leadId), setLeadCommercialStage(db, input)

  • Produces migrations v15 tables consumed by all later tasks

  • Step 1: Write failing migration and seed tests

const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((r) => r.name);
assert.ok(tables.includes('outreach_campaigns'));
assert.ok(tables.includes('outreach_campaign_variants'));
assert.ok(tables.includes('outreach_campaign_queue'));
assert.ok(tables.includes('outreach_inbound_replies'));
assert.ok(tables.includes('outreach_lead_states'));
assert.ok(tables.includes('outreach_stage_events'));
assert.ok(tables.includes('outreach_appointments'));
seedCampaigns(db);
assert.deepEqual(listCampaigns(db).map((c) => c.produto), ['G-Delivery', 'GTI Vendas', 'GTI Vendas']);
  • Step 2: Run tests and verify RED

Run: node --test tests/campaign-store.test.mjs tests/db.test.mjs tests/personal-branding.test.mjs

Expected: FAIL because v15, campaign catalog, and campaign helpers do not exist and branding tests still reject approved Grupo GTI names.

  • Step 3: Append migration v15

Add these table responsibilities and constraints in one appended migration:

CREATE TABLE outreach_campaigns (
  id INTEGER PRIMARY KEY,
  code TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL,
  produto TEXT NOT NULL CHECK (produto IN ('GTI Vendas','G-Delivery')),
  status TEXT NOT NULL DEFAULT 'paused' CHECK (status IN ('draft','paused','ready','running','completed')),
  pause_reason TEXT,
  week_number INTEGER NOT NULL DEFAULT 1 CHECK (week_number BETWEEN 1 AND 4),
  approved_daily_limit INTEGER NOT NULL DEFAULT 10 CHECK (approved_daily_limit IN (10,12,15,18)),
  segments_json TEXT NOT NULL,
  windows_json TEXT NOT NULL DEFAULT '[{"start":"09:30","end":"11:30"},{"start":"14:00","end":"17:00"}]',
  interval_min_ms INTEGER NOT NULL DEFAULT 720000,
  interval_max_ms INTEGER NOT NULL DEFAULT 1500000,
  hourly_limit INTEGER NOT NULL DEFAULT 4,
  verification_multiplier REAL NOT NULL DEFAULT 1.5,
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at TEXT
);

CREATE TABLE outreach_campaign_variants (
  id INTEGER PRIMARY KEY,
  campaign_id INTEGER NOT NULL REFERENCES outreach_campaigns(id) ON DELETE CASCADE,
  code TEXT NOT NULL CHECK (code IN ('A','B','C')),
  mensagem TEXT NOT NULL,
  ativo INTEGER NOT NULL DEFAULT 1,
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  UNIQUE(campaign_id, code)
);

CREATE TABLE outreach_campaign_queue (
  id INTEGER PRIMARY KEY,
  campaign_id INTEGER NOT NULL REFERENCES outreach_campaigns(id) ON DELETE CASCADE,
  lead_id INTEGER NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
  variant_id INTEGER NOT NULL REFERENCES outreach_campaign_variants(id),
  numero TEXT NOT NULL,
  segmento TEXT NOT NULL,
  rendered_message TEXT NOT NULL,
  selection_reason TEXT NOT NULL,
  status TEXT NOT NULL CHECK (status IN ('preview','approved','scheduled','reserved','sent','cancelled','unknown','error')),
  scheduled_at TEXT,
  outreach_send_id INTEGER REFERENCES outreach_sends(id),
  cancel_reason TEXT,
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at TEXT
);
CREATE UNIQUE INDEX idx_campaign_queue_active_lead
  ON outreach_campaign_queue(campaign_id, lead_id)
  WHERE status IN ('preview','approved','scheduled','reserved','sent','unknown');
CREATE INDEX idx_campaign_queue_due ON outreach_campaign_queue(status, scheduled_at);

CREATE TABLE outreach_inbound_replies (
  id INTEGER PRIMARY KEY,
  provider_message_id TEXT NOT NULL UNIQUE,
  lead_id INTEGER REFERENCES leads(id) ON DELETE SET NULL,
  numero TEXT NOT NULL,
  received_at TEXT NOT NULL,
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE outreach_lead_states (
  lead_id INTEGER PRIMARY KEY REFERENCES leads(id) ON DELETE CASCADE,
  stage TEXT NOT NULL,
  first_reply_at TEXT,
  updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE outreach_stage_events (
  id INTEGER PRIMARY KEY,
  lead_id INTEGER NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
  campaign_id INTEGER REFERENCES outreach_campaigns(id) ON DELETE SET NULL,
  stage TEXT NOT NULL,
  note TEXT,
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE outreach_appointments (
  id INTEGER PRIMARY KEY,
  lead_id INTEGER NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
  campaign_id INTEGER REFERENCES outreach_campaigns(id) ON DELETE SET NULL,
  kind TEXT NOT NULL CHECK (kind IN ('demonstracao','teste_g_delivery')),
  participant_name TEXT,
  company_name TEXT,
  preferred_day TEXT,
  preferred_time TEXT,
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

Add lead-linked campaign tables to resetLeadsBase deletion order; preserve opt-outs.

  • Step 4: Implement catalog and store helpers

Define three campaigns with exact codes gdelivery-food, gti-food, gti-retail; exact segment arrays from the specification; exact products; and six unique catalog variants. Seed A/B/C G-Delivery rows into gdelivery-food and seed the same A/B/C GTI Vendas texts into both GTI campaigns, producing nine campaign-owned rows from six unique approved texts. seedCampaigns inserts missing campaign/variant rows but does not overwrite an edited status, week, or approved limit.

  • Step 5: Run focused tests and verify GREEN

Run: node --test tests/campaign-store.test.mjs tests/db.test.mjs tests/personal-branding.test.mjs

Expected: PASS; migration remains idempotent; reset deletes lead-linked campaign records and preserves opt-outs.

Task 2: Safe Campaign Message Rendering

Files:

  • Create: src/outreach/campaign-messages.mjs
  • Test: tests/campaign-messages.test.mjs

Interfaces:

  • Produces: naturalBusinessName(lead): string

  • Produces: managementFocus(segment): string

  • Produces: renderCampaignMessage(template, lead): string

  • Produces: variantForLead(variants, leadId): object

  • Step 1: Write failing rendering tests

assert.equal(naturalBusinessName({ empresa: '61.590.668 A ARAUJO', legal_name: 'A ARAUJO LTDA' }), '');
assert.equal(naturalBusinessName({ empresa: 'Pizzaria Central' }), 'Pizzaria Central');
assert.equal(managementFocus('restaurantes'), 'vendas, estoque, comandas e emissão fiscal');
assert.equal(variantForLead([{ code: 'A' }, { code: 'B' }, { code: 'C' }], 4).code, 'B');
assert.doesNotMatch(renderCampaignMessage(template, lead), /CNPJ|R\$|http/i);
  • Step 2: Run test and verify RED

Run: node --test tests/campaign-messages.test.mjs

Expected: FAIL because the focused campaign renderer does not exist.

  • Step 3: Implement deterministic safe rendering

Use leadId % activeVariants.length for stable A/B/C assignment. Replace {foco_gestao} and the opening greeting. When the trade name is natural, render Olá, pessoal da {nome_fantasia}, tudo bem?; otherwise preserve Olá, tudo bem? or Olá! according to the approved variant. Reject a rendered initial message when it contains a URL, a CNPJ-shaped 14-digit number, R$, or lacks both Gustavo and Grupo GTI or the refusal sentence não volto a entrar em contato.

  • Step 4: Run focused tests and verify GREEN

Run: node --test tests/campaign-messages.test.mjs tests/scoring.test.mjs

Expected: PASS.

Task 3: Gradual Verification and Queue Preview

Files:

  • Create: src/outreach/campaign-queue.mjs
  • Modify: src/outreach/campaign-store.mjs
  • Test: tests/campaign-queue.test.mjs

Interfaces:

  • Produces: prepareDailyQueue({ db, whatsapp, campaignId, now }): Promise<{checked, previewed, skipped}>

  • Produces: listCampaignQueue(db, campaignId, status?)

  • Consumes exact helpers from Tasks 1-2 and existing verification/opt-out helpers

  • Step 1: Write failing eligibility and cap tests

Cover campaign segment order, no prior product send, latest confirmed equivalent number, opt-out, inbound-replied state, active queue dedupe, stable variants, and exact verification cap:

const summary = await prepareDailyQueue({ db, whatsapp, campaignId, now });
assert.ok(summary.checked <= Math.ceil(campaign.approved_daily_limit * 1.5));
assert.ok(summary.previewed <= campaign.approved_daily_limit);
assert.equal(new Set(queue.map((q) => q.lead_id)).size, queue.length);
  • Step 2: Run test and verify RED

Run: node --test tests/campaign-queue.test.mjs

Expected: FAIL because no campaign eligibility/preparation service exists.

  • Step 3: Implement sparse candidate selection and verification

Query only the active campaign's segment list, ordered by prioridade ASC, score DESC, id ASC, and exclude prior same-product outreach_sends, replies, opt-outs, and active queue rows in SQL. Process already-confirmed candidates first. Call whatsapp.checkNumber sequentially for at most ceil(1.5 * approved_daily_limit) unverified candidates, persist every result, and never retry erro_temporario in the same preparation run.

Insert preview rows transactionally with immutable variant ID, rendered text, normalized number, segment, and a selection reason such as gdelivery-food:prioridade-1.

  • Step 4: Verify no full-base scan

Add a test with 10,000 out-of-segment leads and inspect EXPLAIN QUERY PLAN; require idx_leads_segmento or another selective campaign index and bounded candidate results.

  • Step 5: Run focused tests and verify GREEN

Run: node --test tests/campaign-queue.test.mjs tests/db.test.mjs

Expected: PASS with no provider send calls.

Task 4: Scheduling, Approval, and Campaign API

Files:

  • Create: src/outreach/campaign-schedule.mjs
  • Create: src/server/routes/campaigns.mjs
  • Modify: src/server/index.mjs
  • Test: tests/campaign-schedule.test.mjs
  • Test: tests/campaign-routes.test.mjs

Interfaces:

  • Produces: generateDailySchedule({ date, count, windows, minMs, maxMs, hourlyLimit, random }): Date[]

  • Produces routes under /api/campaigns

  • Consumes prepareDailyQueue and campaign-store queue transitions

  • Step 1: Write failing schedule tests

Assert Monday-Friday only, exact windows, 12-25 minute requested spacing, no more than four timestamps in any rolling 60 minutes, deterministic injected random, lunch break, and at most 18 slots.

  • Step 2: Run schedule tests and verify RED

Run: node --test tests/campaign-schedule.test.mjs

Expected: FAIL because schedule generation does not exist.

  • Step 3: Implement schedule generation

Advance through both windows in local time. For each random interval, enforce candidate >= fourthMostRecent + 60 minutes before accepting. If no valid slot remains, return fewer timestamps; never spill into lunch, another date, or weekend.

  • Step 4: Write failing route tests

Required admin routes:

GET    /api/campaigns
GET    /api/campaigns/:id
POST   /api/campaigns/:id/prepare
GET    /api/campaigns/:id/queue
POST   /api/campaigns/:id/approve
DELETE /api/campaigns/:id/queue/:queueId
POST   /api/campaigns/:id/start
POST   /api/campaigns/:id/pause
PUT    /api/campaigns/:id/week

Assert non-admin mutation is 403; approval accepts explicit queue IDs only; deletion cannot remove reserved/sent rows; start requires at least one scheduled row and WhatsApp READY; week changes map exactly 1->10, 2->12, 3->15, 4->18 and never advance implicitly.

  • Step 5: Implement transactional transitions and routes

Approval schedules selected preview rows and cancels unselected rows with cancel_reason='removed_by_operator'. Campaign start changes ready|paused -> running; pause stores a required reason. Return manual and campaign daily usage as separate fields.

  • Step 6: Run route tests and verify GREEN

Run: node --test tests/campaign-schedule.test.mjs tests/campaign-routes.test.mjs tests/server-whatsapp-integration.test.mjs

Expected: PASS.

Task 5: Due Queue Worker, Recovery, and Separate Limits

Files:

  • Create: src/outreach/campaign-worker.mjs
  • Modify: src/db.mjs
  • Modify: src/outreach/scheduler.mjs
  • Modify: src/outreach/run.mjs
  • Modify: scripts/servidor.mjs
  • Modify: scripts/enviar-outreach.mjs
  • Test: tests/campaign-worker.test.mjs
  • Modify: tests/scheduler.test.mjs
  • Modify: tests/server-shutdown.test.mjs

Interfaces:

  • Produces: runDueCampaignQueue({ db, whatsapp, now, signal, log }): Promise<summary>

  • Produces: pauseCampaignsAfterRestart(db): number

  • Produces: reserveCampaignQueueSend(db, { queueId, now }): { queueId, sendId }

  • Changes reserveOutreachSend so its manual limit query counts only origem='manual'; automatic campaign limits are enforced by reserveCampaignQueueSend

  • Step 1: Write failing separate-limit and due-item tests

Assert manual sends count only origem='manual'; campaign sends count only the campaign's automatic attempts. Due worker sends only scheduled_at <= now, never preview/approved/future rows, and handles one due row at a time.

  • Step 2: Run worker tests and verify RED

Run: node --test tests/campaign-worker.test.mjs tests/outreach-routes.test.mjs tests/scheduler.test.mjs

Expected: FAIL on missing worker and shared-origin limit.

  • Step 3: Implement atomic campaign reservation

Inside whatsapp.sendText.beforeSend, run one transaction that re-reads queue/campaign/lead state and requires: campaign running, queue scheduled and due, READY connection, confirmed number, no equivalent opt-out, no inbound reply, no same-product active send, automatic count below campaign limit, and rolling-hour attempts below four. Transition queue to reserved and create outreach_sends together; return both IDs.

In afterSend, finalize provider history and queue status exactly once. Map delivery unknown to queue unknown, definite error to error, and success to sent.

  • Step 4: Replace raw-lead scheduler execution

Run the due worker every 60 seconds while retaining overlap prevention and abort-aware shutdown. Make runOutreachBatch and scripts/enviar-outreach.mjs consume the same approved due queue; neither may select raw leads.

  • Step 5: Implement restart safety

Before scheduler start, set every running campaign to paused with pause_reason='server_restart', set legacy outreach.ativo=false, recover reserved queue rows by joining outreach_sends, and preserve scheduled rows without sending until explicit start.

  • Step 6: Run focused tests and verify GREEN

Run: node --test tests/campaign-worker.test.mjs tests/scheduler.test.mjs tests/server-shutdown.test.mjs tests/outreach-routes.test.mjs tests/outreach.test.mjs

Expected: PASS; no raw-lead automatic path remains.

Task 6: Inbound Reply Detection and Send-Race Protection

Files:

  • Modify: src/whatsapp/manager.mjs
  • Create: src/outreach/inbound.mjs
  • Modify: scripts/servidor.mjs
  • Modify: src/outreach/campaign-worker.mjs
  • Test: tests/whatsapp-manager.test.mjs
  • Test: tests/inbound-outreach.test.mjs

Interfaces:

  • Adds manager option onInboundMessage(event)

  • Event: { messageId: string, jid: string, number: string, receivedAt: string }

  • Produces: recordInboundReply(db, event): { inserted, leadId, cancelled }

  • Step 1: Write failing manager event tests

Emit messages.upsert and assert only new, non-fromMe, one-to-one @s.whatsapp.net messages produce one callback. Ignore groups, broadcasts, status JIDs, duplicates, and messages without an ID.

  • Step 2: Run manager tests and verify RED

Run: node --test tests/whatsapp-manager.test.mjs

Expected: FAIL because inbound events are not subscribed.

  • Step 3: Implement provider-neutral inbound event publication

Do not classify text or send a reply. Normalize only the sender number and timestamp; preserve existing outbound messages.update behavior.

  • Step 4: Write failing persistence/race tests

For an inbound number, find the latest campaign send across equivalent variants, insert by unique provider ID, set current stage respondeu, set first_reply_at once, append a stage event, and cancel every preview/approved/scheduled row for equivalent numbers. Reprocessing the same provider ID is idempotent.

Block a worker in beforeSend, record an inbound reply, then release it; assert provider sendMessage is never called.

  • Step 5: Implement inbound transaction and startup injection

Inject onInboundMessage: (event) => recordInboundReply(db, event) in scripts/servidor.mjs. Log persistence failures without throwing through the provider EventEmitter.

  • Step 6: Run focused tests and verify GREEN

Run: node --test tests/whatsapp-manager.test.mjs tests/inbound-outreach.test.mjs tests/campaign-worker.test.mjs

Expected: PASS and no automatic outbound reply.

Task 7: Manual Pipeline and Appointment API

Files:

  • Modify: src/outreach/campaign-store.mjs
  • Modify: src/server/routes/campaigns.mjs
  • Modify: src/treatment/queries.mjs
  • Test: tests/pipeline.test.mjs
  • Test: tests/campaign-routes.test.mjs

Interfaces:

  • Produces: listPendingReplies(db)

  • Produces: setLeadCommercialStage(db, { leadId, campaignId, stage, note })

  • Produces: saveAppointment(db, input)

  • Adds current stage/history/appointments to getLead

  • Step 1: Write failing stage and appointment tests

Accept only the 11 specification stages. Every change updates current state and appends history in one transaction. opt_out requires addOptOut for the equivalent phone. demonstracao_agendada requires participant, company, preferred day, and preferred time. teste_g_delivery_iniciado creates a test appointment without demo time requirements.

  • Step 2: Run tests and verify RED

Run: node --test tests/pipeline.test.mjs tests/campaign-routes.test.mjs

Expected: FAIL because stage/appointment APIs do not exist.

  • Step 3: Implement routes and enrichment
GET  /api/campaigns/replies/pending
PUT  /api/campaigns/leads/:leadId/stage
POST /api/campaigns/leads/:leadId/appointments

Return stage history newest first, appointments newest first, and provider/send history without duplicating ACK fields.

  • Step 4: Run focused tests and verify GREEN

Run: node --test tests/pipeline.test.mjs tests/campaign-routes.test.mjs tests/personal-backend.test.mjs

Expected: PASS.

Task 8: Campaign Metrics and Automatic Pause Policies

Files:

  • Create: src/outreach/campaign-metrics.mjs
  • Modify: src/outreach/campaign-worker.mjs
  • Modify: src/outreach/campaign-store.mjs
  • Modify: src/server/routes/campaigns.mjs
  • Test: tests/campaign-metrics.test.mjs

Interfaces:

  • Produces: campaignMetrics(db, campaignId, filters?)

  • Produces: evaluateCampaignHealth(db, campaignId): { pause, reason }

  • Step 1: Write failing metric tests

Cover previewed, approved, removed, attempted, provider statuses, replies, first-response time, positive/negative/opt-out, demos, tests, sales, and grouping by product/segment/variant.

  • Step 2: Write failing pause tests

Exact policies:

unknownCount > 0                                      // unknown_outcome
attemptsToday >= 10 && failureRateToday > 0.10       // high_failure_rate
contacts >= 20 && negativeOrOptOutRate > 0.10        // high_negative_rate
contacts >= 50 && replyRate < 0.05                   // low_reply_rate

Assert equality at 10% does not pause because the requirement says exceeds 10%. Assert an automatic pause never changes itself back to running.

  • Step 3: Run tests and verify RED

Run: node --test tests/campaign-metrics.test.mjs

Expected: FAIL because campaign metrics/policies do not exist.

  • Step 4: Implement metrics and policy hooks

Evaluate health after every send finalization and manual stage change. On pause, store the reason and cancel no historical rows; future scheduled rows remain visible but ineligible until an explicit resume. A variant receives eligibleForComparison=true only at 40 sends for that exact campaign/segment/variant; declare a winner only when all active compared variants satisfy that threshold.

  • Step 5: Add metrics route and verify GREEN

Route: GET /api/campaigns/:id/metrics?segmento=&variant=

Run: node --test tests/campaign-metrics.test.mjs tests/campaign-routes.test.mjs tests/campaign-worker.test.mjs

Expected: PASS.

Task 9: Supervised Campaign Browser Workspace

Files:

  • Modify: public/index.html
  • Create: public/js/campanhas.js
  • Modify: public/js/app.js
  • Modify: public/js/tratamento.js
  • Modify: public/css/app.css
  • Modify: tests/personal-branding.test.mjs
  • Create: tests/campaign-ui.test.mjs

Interfaces:

  • Adds top-level Campanhas tab

  • Consumes campaign, queue, reply, stage, appointment, and metrics routes

  • Does not construct outbound text in the browser

  • Step 1: Write failing UI behavior tests

Using the existing VM DOM harness, test campaign cards, week/limit display, prepare action, preview rows with exact server-rendered text, per-row removal, explicit approval, start/pause controls, separate manual/automatic usage, pending replies, stage classification, appointment form, metrics filters, and a simulated post-server-restart response showing paused with reason server_restart.

  • Step 2: Run tests and verify RED

Run: node --test tests/campaign-ui.test.mjs tests/personal-branding.test.mjs

Expected: FAIL because no campaign workspace exists.

  • Step 3: Build the focused campaign tab

Keep existing visual language. Use separate sections for campaign status, daily preview, pending replies, funnel, and metrics. Require confirmation before start, weekly increase, and opt-out. Show pause reasons prominently. Never expose an editable campaign message after approval; removal is allowed, mutation is not.

  • Step 4: Extend lead detail

Show current stage, first reply, stage history, campaign/send history, and appointments. Keep manual send available but do not prefill it with browser-generated generic campaign copy.

  • Step 5: Add responsive behavior and verify GREEN

At widths below 760 px, campaign cards stack, tables remain horizontally scrollable, action controls become full-width, and no fixed-width dialog exceeds the viewport.

Run: node --test tests/campaign-ui.test.mjs tests/personal-branding.test.mjs

Expected: PASS; node --check public/js/campanhas.js also passes.

Task 10: Real Browser Acceptance and Final Verification

Files:

  • Modify: package.json
  • Modify: package-lock.json
  • Create: playwright.config.mjs
  • Create: tests/browser/server.mjs
  • Create: tests/browser/campaign-pilot.spec.mjs

Interfaces:

  • Adds npm run test:browser

  • Browser test server uses an isolated temporary SQLite database and fake WhatsApp manager

  • Never connects to or sends through the real provider

  • Step 1: Install the browser test dependency

Run: npm.cmd install --save-dev @playwright/test

Run: npx.cmd playwright install chromium

  • Step 2: Create isolated browser test server

Open a temporary database, seed admin/campaigns/sample leads/verifications, inject a fake READY manager that records sends in memory, and start createAppServer on 127.0.0.1:3911. Delete the temporary database on shutdown.

  • Step 3: Write desktop and mobile acceptance tests

Configure Chromium projects at 1440x900 and 390x844. Seed one pending inbound reply in the isolated test database before browser startup. Test login, campaign prepare, preview removal, approval, explicit start, separate limits, pause reason, pending reply classification, demonstration scheduling, and metrics rendering. Assert fake provider send count remains zero unless the test advances a due scheduled item explicitly.

  • Step 4: Run all automated verification

Run: npm.cmd test

Expected: all Node tests pass.

Run: npm.cmd run test:browser

Expected: desktop and mobile projects pass with no console errors.

Run: npm.cmd audit --omit=dev

Expected: zero production vulnerabilities.

  • Step 5: Restart and perform non-sending smoke checks

Verify migration v15, server-restart campaign pause, WhatsApp status, campaign preview, manual and automatic counters, queue approval, pending-reply UI, and mobile layout. Do not start a real campaign or send a real message without Gustavo's explicit recipient-level authorization.