📄 SKILL.md

← Vault

name: comercialrs-debug-workflow

description: Supabase Cloud CRM project on VPS — schema, Edge Functions patterns, WhatsApp dispatch, link auto-fill debugging, and critical file-loss lessons


ComercialRS Debug & Sync Workflow

Context

Supabase Cloud project dauftiqcvgaydddoxqhh for a sales CRM site (comercialrs.rochasalesseguros.com.br) deployed on VPS with Docker/nginx.


Critical Lesson: rsync OVERWRITES, does not merge

`bash

WRONG — trailing slash on SOURCE means "copy contents of commercialrs-main into comercialrs/"

rsync -avz comercialrs-main/ /var/www/comercialrs/

`

This deletes everything in /var/www/comercialrs/ that doesn't exist in commercialrs-main/ before copying. The src/ directory was wiped out.

`bash

Option A — only copy specific files you need

rsync -avz comercialrs-main/src/components/ /var/www/comercialrs/src/components/

Option B — manual copy of changed files only

cp -r comercialrs-main/src/components/onboarding/ /var/www/comercialrs/src/components/onboarding/

`

ALWAYS verify what's in the target before rsyncing.


Critical Lesson: .env is REQUIRED before rebuild

Without .env, the Vite build does NOT embed VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY, causing runtime error:

`

supabaseUrl is required.

`

Order of operations for restore:

1. Copy .env FIRST

2. npm install

3. npm run build

4. Verify in dist/assets/*.js that the Supabase URL appears

`bash

Check if .env was embedded in the bundle

grep -a "dauftiqcvgaydddoxqhh" /var/www/comercialrs/dist/assets/*.js

`


VPS Container/Port Map (CRITICAL)

Port discovery commands:

`bash

docker ps --format "{{.Names}} {{.Ports}}" # all containers + ports

ss -tlnp 2>/dev/null | grep LISTEN # all listening ports on host

docker network inspect deploy-vps_default # Docker internal IPs (172.x.x.x)

`

Supabase CLI db query limitation: supabase db query connects to LOCAL Supabase (Docker), NOT the linked cloud project — even after supabase link. Use Management API or Edge Functions to manage cloud DB.

Management API 403: api.supabase.com/v1/projects/{ref}/database/query returns 403 error code: 1010 (Cloudflare/proxy block). Cannot create tables via Management API. Workaround: Edge Function with service role (but Edge Functions cannot run raw SQL DDL). Table creation on Supabase Cloud must be done via Supabase Dashboard UI.


Database Schema (current, verified)

Tables


dispatch-automation — COMPLETELY REWRITTEN (multiple stacked bugs fixed)

Symptoms: Brokers don't receive WhatsApp notifications when meetings are created.

Bugs found and fixed (cumulative):

1. PostgREST null filter: is=deleted_at.nulldeleted_at=is.null

2. Not actually sending WhatsApp: The function only returned data, never called UAZAPI

3. Wrong UAZAPI endpoint: Was using /messages/send → correct is /send/text?token=

4. Token in wrong header: Authorization: Bearer → query param ?token=

5. event_type vs trigger mismatch: Frontend sends event_type, old code only read trigger — added support for both

6. task_created going to scheduled switch instead of instant: Frontend calls dispatch-automation with automation_key: 'whatsapp_reminders' + event_type: 'task_created' + payload. Code was checking instant triggers but then had an else if (key) that caught everything else. Fixed: instant triggers checked FIRST with effectiveTrigger guard, scheduled jobs in else if (key) block.

Code architecture (correct):

`typescript

// Instant triggers — fire IMMEDIATELY when task_created

if (isUazapi && config && effectiveTrigger && payload) {

if (effectiveTrigger === 'task_created') { / send WhatsApp NOW / }

}

// Scheduled jobs — run on cron, no payload

else if (key) {

switch (key) {

case 'whatsapp_reminders': { / daily reminder check / }

// ...

}

}

`

Critical column names in automations table: key (not automation_key), status (not active), trigger_type (null for most), last_run_at. The table does NOT have event_type or trigger columns — PostgREST filters on those columns return 400.

Frontend call pattern (automationDispatcher.ts):

`typescript

dispatchAutomation('whatsapp_reminders', 'task_created', { title, due_date, channel, link });

// Sends: { automation_key: 'whatsapp_reminders', event_type: 'task_created', payload: {...} }

`

Message format for task_created:

`

📹 Nova Reunião Criada

📌 {title}

📅 {date}

🔗 {link}

`

(Channel emoji: 📹 video, 📞 telefone, 💬 mensagem)

Test command:

`bash

curl -s -X POST "https://dauftiqcvgaydddoxqhh.supabase.co/functions/v1/dispatch-automation" \

-H "Authorization: Bearer eyJ_REDACTED" \

-H "Content-Type: application/json" \

-d '{"meetingId":"","event_type":"meeting_created"}'

`

Verify sent:

`bash

curl -s "https://dauftiqcvgaydddoxqhh.supabase.co/rest/v1/whatsapp_logs?select=*&order=sent_at.desc&limit=5" \

-H "apikey: " -H "Authorization: Bearer "

`

Tested: 9 messages sent successfully to brokers.

dispatch-automation Event Types

  • - task_created — sent when a new meeting/task is created via AutomationsPanel.handleTaskCreated
  • - reminder_check — cron-triggered reminder check
  • - ai_analysis, weekly_report, daily_briefing, whatsapp_reminders, overdue_alerts, risk_alerts, weekly_closing — other automation types

  • Debugging Commands (Supabase Cloud)

    read-config function — shows automations_config, auth_users

    `bash

    curl -s -X POST "https://dauftiqcvgaydddoxqhh.supabase.co/functions/v1/read-config" \

    -H "Content-Type: application/json" \

    -H "Authorization: Bearer " | python3 -m json.tool

    `

    db-inspect function — shows profiles, team_members, tasks counts

    `bash

    curl -s -X POST "https://dauftiqcvgaydddoxqhh.supabase.co/functions/v1/db-inspect" \

    -H "Content-Type: application/json" \

    -H "Authorization: Bearer "

    `

    inspect-db function — lightweight check (tasks, members, profiles)

    `bash

    curl -s -X POST "https://dauftiqcvgaydddoxqhh.supabase.co/functions/v1/inspect-db" \

    -H "Content-Type: application/json" \

    -H "Authorization: Bearer "

    `

    Verified Working Edge Functions (currently deployed)

  • - sync-profiles — updates/creates profiles from JSON array
  • - sync-all-tasks — deletes all tasks and bulk inserts from JSON (354 tasks) ⚠️ deletes all before import
  • - db-inspect — reads profiles, tasks, team_members for debugging
  • - admin-reset-user-password — resets user password via PUT /auth/v1/admin/users/{uid}
  • - admin-update-user-email — updates user email
  • - firecrawl-extract-meeting — extracts meeting data by UUID search in link/external_ref/title
  • - dispatch-automation — sends WhatsApp via UAZAPI direct, logs to whatsapp_logs. COMPLETELY REWRITTEN.
  • - resend-pending-meetings — sends WhatsApp reminders for pending tasks. RECREATED after rsync deletion.

  • Edge Functions DELETED by rsync — must recreate if needed

  • - crm-lead-sync, crm-mirror-health, send-reminders, sales-overdue-alerts, invite-member, register-uazapi-webhook, uazapi-webhook, test-broadcast, get-roles, admin-create-user, sync-agent-permissions
  • - ⚠️ Source code no longer exists locally for most of these. resend-pending-meetings and dispatch-automation have been RECREATED and deployed.

  • WhatsApp / UAZAPI Reconnection (CRITICAL — Disconnected Session)

    Symptom: dispatch-automation returns whatsapp_sent: 1 but broker never receives messages. Test calls to /send/text return {"error":true,"message":"WhatsApp disconnected"}.

    Root cause: Instance disconnected — "lastDisconnectReason": "401: logged out from another device with recentMessage". Happens when someone logs in from another device or during webhook configuration work.

    Diagnosis:

    `bash

    curl -s "https://rochasales.uazapi.com/instance/status" \

    -H "token: 94f48fd6-ed40-4654-bdd7-22f8b34bb356"

    `

    Check "status": "connected" / "loggedIn": true in response.

    Reconnection procedure:

    `bash

    1. Generate new QR code

    curl -s -X POST "https://rochasales.uazapi.com/instance/connect" \

    -H "token: 94f48fd6-ed40-4654-bdd7-22f8b34bb356" \

    -H "Content-Type: application/json" -d '{}'

    2. Extract QR from response JSON (base64 PNG data URL)

    Save as /root/uazapi_qr.png and send to user as MEDIA:/root/uazapi_qr.png

    3. User scans with WhatsApp app (WhatsApp → Settings → Linked Devices → Connect Device)

    `

    After reconnection, VERIFY:

    `bash

    curl -s "https://rochasales.uazapi.com/instance/status" -H "token: ..."

    Should show "status": "open" or "connected": true

    `

    Documents App — Separate Supabase Project (DocCorretor)

    Supabase project: dauftiqcvgaydddoxqhh (same as ComercialRS)

    VPS app path: /var/www/documentos/deploy-vps-nodejs/app

    Live URL: https://documentos.rochasalesseguros.com.br

    Server: systemd service documentos.service, port 3010

    Service Role Key Rotation — CRITICAL

    The SUPABASE_SERVICE_ROLE_KEY in the app's .env was INVALIDATED (key was rotated).

  • - Anon key (public): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... — works for reads only
  • - Service role key (.env): eyJ_REDACTEDINVALID, returns {"message":"Invalid API key"}
  • Workaround for writes (table create/delete/update):

    1. Dashboard UI — go to https://supabase.com/dashboard/project/dauftiqcvgaydddoxqhh/table-editor and run SQL directly in the SQL Editor

    2. supabase CLI limitationsupabase db query connects to LOCAL Supabase (Docker), NOT the linked cloud project, even after supabase link. Cannot be used for cloud DB operations.

    Documents App Tables (from code grep — partial list)

    The app uses these tables (some may not exist in the project yet):

  • - profiles — user profiles (EXISTS, has duplicate data from May 13 2026)
  • - user_roles — user roles (EXISTS, has duplicate data from May 13 2026)
  • - roles — roles reference table (DOES NOT EXIST — referenced by adm.usuarios.tsx)
  • - allowed_emails — email whitelist (DOES NOT EXIST — referenced by adm.allowlist.tsx)
  • - role_permissions — role permissions (DOES NOT EXIST — referenced by code)
  • - documents_access — document access control (DOES NOT EXIST)
  • - audit_log — audit log (DOES NOT EXIST)
  • - client_upload_links — upload links (DOES NOT EXIST)
  • - cms_blocks, cms_content, cms_media, cms_menu — CMS tables (DO NOT EXIST)
  • - clients — clients (EXISTS?)
  • - documents — documents (EXISTS?)
  • - gmail_connections — Gmail OAuth (EXISTS)
  • Cloud Storage JWT_SECRET Mismatch (CRITICAL — Jun 2026)

    Symptom: Both anon key AND service_role key fail Cloud storage with:

  • - Anon key → {"statusCode":"403","error":"Unauthorized","message":"Invalid Compact JWS"}
  • - Service role key → {"statusCode":"403","error":"Unauthorized","message":"signature verification failed"}
  • Root cause: The keys in .env were signed with a JWT_SECRET that does NOT match the Cloud project's JWT_SECRET. Possible causes: (1) project was recreated after keys were generated, (2) JWT_SECRET was rotated in the Cloud project after key generation.

    Supabase NEVER exposes JWT_SECRET via any API (security reason). Cannot verify/regenerate programmatically.

    Diagnosis commands:

    `bash

    List projects + refs

    npx supabase projects list

    Output:

    dauftiqcvgaydddoxqhh | Dashboard Rocha Sales Meta e CRM (West US)

    nzaxsorgsecnmqwufawn | DocCorretor (East US)

    Get API keys (but NOT JWT_SECRET)

    npx supabase projects api-keys list --project-ref dauftiqcvgaydddoxqhh

    Get server-side secrets (JWKS, DB_URL — but NOT JWT_SECRET)

    npx supabase secrets list --project-ref dauftiqcvgaydddoxqhh

    `

    Two possible fixes:

    Option A (Dashboard — fastest):

    1. Go to https://supabase.com/dashboard/project/dauftiqcvgaydddoxqhh/settings/api

    2. Find "JWT_secret" → click "Regenerate"

    3. Copy new anon key and service_role key

    4. Update .env with new keys

    5. Rebuild and restart server

    Option B (Personal Access Token — for automation):

    1. Create PAT at https://supabase.com/dashboard/account/tokens

    2. Use management API to regenerate keys:

    `bash

    curl -X POST "https://api.supabase.com/v1/projects/dauftiqcvgaydddoxqhh/api-keys" \

    -H "Authorization: Bearer " \

    -H "Content-Type: application/json" \

    -d '{"type":"access_token"}'

    `

    CLI link pitfall: After supabase link --project-ref dauftiqcvgaydddoxqhh, the supabase/config.toml still shows old project_id = "lzoiuxulhnvtmadoymrb". This is NORMAL — the project_id in config.toml is an internal ID, different from the project ref used in URLs. Use npx supabase projects list to confirm which project is actually linked.

    What still works: Auth (login/logout), REST API reads, Edge Functions — only Cloud storage is broken.

    Known Issues

  • - Management API 403: Cannot create tables via Management API — must use Supabase Dashboard UI
  • - Malformed dates in tasks.due_date: Some imported tasks have invalid dates like 2026-04-13T80:00 (80 minutes). These produce date: null in firecrawl-extract-meeting responses.
  • - crm_leads is STALE: Latest entry is 2026-03-31. Always use crm_leads_duplicate for live data.
  • - crm_leads_duplicate has no meeting fields: custom_data.timeline only has NegocioCriado, AnotacaoRegistrada, NegocioTransferido, NegocioMovido. No Reuniao or schedule types exist. Automatic meeting creation from database alone is NOT possible without Painel webhook.
  • - Painel fechamento is always null: The GraphQL API returns null for ALL deals' fechamento field. There is NO structured meeting date field. The date must come from user manual input or annotation parsing (e.g., "COTAÇÃO VIA WHATS 05.05" → 2026-05-05).
  • - Webhook endpoint pending: User needs to provide n8n webhook URL for create-painel-webhook function. Edge function is written but not deployed.

  • Bug 1: Kanban não atualiza após criar tarefa (✅ CORRIGIDO — Jun 2026)

    Sintoma: Ao criar nova reunião, o Kanban não mostra a tarefa nova. Usuário precisa atualizar página manualmente.

    Causa raiz: O KanbanBoard (KanbanBoard.tsx) mantém um estado local useState(externalTasks) que só sincroniza quando a referência do array muda:

    `typescript

    useEffect(() => {

    setTasks(externalTasks); // só roda se a referência do array mudou

    }, [externalTasks]);

    `

    O AppDataContext atualiza via realtime (mesma referência de array) — o Kanban nunca detecta.

    Fix aplicado (Jun 2026) — src/components/tasks/KanbanBoard.tsx:

    `typescript

    // Track the external task IDs to force re-sync when realtime updates arrive

    const [lastTaskIds, setLastTaskIds] = useState(() => externalTasks.map(t => t.id).join(','));

    useEffect(() => {

    const newIds = externalTasks.map(t => t.id).join(',');

    if (newIds !== lastTaskIds) {

    setLastTaskIds(newIds);

    setTasks(externalTasks);

    }

    }, [externalTasks, lastTaskIds]);

    `

    Compara IDs em vez de referência de array — mudanças via realtime são detectadas.

    Deploy: Build local funciona (npm run build). Deploy para VPS via rsync/scp não funciona — SSH credentials recusadas. O arquivo foi modificado localmente em /tmp/comercialrs/comercialrs-main/src/components/tasks/KanbanBoard.tsx e precisa ser enviada ao VPS.


    Bug 2: WhatsApp Response → Task Status Update (✅ CORRIGIDO — Jun 2026)

    Cadeia completa (como deve funcionar)

    1. dispatch-automation envia template Meta WhatsApp para o corretor

    2. dispatch-automation grava meta_message_id + task_id na message_log

    3. Meta entrega mensagem → corretor recebe no WhatsApp

    4. Corretor responde "iniciada" / "concluída" / "remarketing"

    5. Meta envia webhook para meta-webhook

    6. meta-webhook extrai msgContext.id (o ID da nossa mensagem)

    7. meta-webhook chama RPC get_task_by_meta_message_id(msgContext.id) → obtém task_id

    8. meta-webhook atualiza tasks.status via RPC

    O que estava quebrado (Jun 2026)

    1. message_log e incoming_messages tables não existiam — todas as tabelas e RPCs foram criadas via SQL no Dashboard.

    2. dispatch-automation NÃO DISPARAVA task_createdBUG PRINCIPAL (Jun 2026)

    O fluxo do dispatch-automation para salvar meta_message_id estava tecnicamente correto no código (sendToMetaRecipientslogMessageSent → POST /rest/v1/message_log). MAS o addTask no AppDataContext.tsx nunca chamava dispatchAutomation com event_type: 'task_created'. O dispatch-automation só era chamado para task_overlap e task_overload. Résultat: nenhum envio real era feito, message_log só tinha 2 registros de teste antigos.

    Fix aplicado (Jun 2026):

  • - ✅ AppDataContext.tsx — adicionado id: task.id nos payloads de task_created e task_updated
  • - ✅ addTask — logs de erro melhorados com detalhes do Supabase (code, message, details, hint)
  • - ✅ dispatch-automation — fluxo meta_message_id está correto e completo (não precisava de mudança)
  • - ✅ Build: nvm use 20 && npm run build
  • - ✅ Tables + RPCs: criadas via SQL no Dashboard
  • - ✅ REPLICA IDENTITY FULL em tasks — sem isso, realtime não emitia eventos UPDATE (causava erro 55000)
  • - ✅ meta_webhook_vps.py no VPS — schema corrigido de auth. para public., message_repliesincoming_messages
  • - ✅ Publication supabase_realtime adicionada: tasks, message_log, incoming_messages
  • ⚠️ FALTA IMPLEMENTAR no AppDataContext.addTask:

    O addTask ainda NÃO chama dispatchAutomation('whatsapp_reminders', 'task_created', payload) após inserir a tarefa. Sem isso, o corretor nunca recebe WhatsApp e o meta_message_id nunca é gravado. Código necessário:

    `typescript

    // Após insert com sucesso, antes do return true:

    dispatchAutomation('whatsapp_reminders', 'task_created', {

    id: task.id,

    title: task.title,

    due_date: task.dueDate,

    channel: task.channel,

    link: task.link,

    responsible_id: task.responsible?.id,

    responsible: task.responsible?.name,

    });

    `

    SQL de setup (executar no Supabase Dashboard → SQL Editor)

    `sql

    -- 1. Tabela message_log

    CREATE TABLE IF NOT EXISTS public.message_log (

    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,

    meta_message_id TEXT UNIQUE,

    task_id UUID REFERENCES public.tasks(id) ON DELETE SET NULL,

    responsible_id UUID REFERENCES public.team_members(id) ON DELETE SET NULL,

    template_name TEXT,

    phone TEXT,

    status TEXT DEFAULT 'sent',

    created_at TIMESTAMPTZ DEFAULT now(),

    updated_at TIMESTAMPTZ DEFAULT now()

    );

    ALTER TABLE public.message_log ENABLE ROW LEVEL SECURITY;

    CREATE POLICY "message_log_insert_service" ON public.message_log FOR INSERT WITH CHECK (true);

    CREATE POLICY "message_log_select_service" ON public.message_log FOR SELECT USING (true);

    -- 2. Tabela incoming_messages

    CREATE TABLE IF NOT EXISTS public.incoming_messages (

    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,

    message_id TEXT UNIQUE,

    from_number TEXT,

    timestamp BIGINT,

    msg_type TEXT,

    body TEXT,

    parsed_status TEXT,

    task_id UUID REFERENCES public.tasks(id) ON DELETE SET NULL,

    created_at TIMESTAMPTZ DEFAULT now()

    );

    ALTER TABLE public.incoming_messages ENABLE ROW LEVEL SECURITY;

    CREATE POLICY "incoming_messages_insert_service" ON public.incoming_messages FOR INSERT WITH CHECK (true);

    CREATE POLICY "incoming_messages_select_service" ON public.incoming_messages FOR SELECT USING (true);

    -- 3. RPCs

    CREATE OR REPLACE FUNCTION public.get_task_by_meta_message_id(p_meta_message_id TEXT)

    RETURNS TABLE(task_id UUID, responsible_id UUID, phone TEXT)

    LANGUAGE plpgsql SECURITY DEFINER AS $$

    BEGIN

    RETURN QUERY

    SELECT ml.task_id, ml.responsible_id, ml.phone

    FROM public.message_log ml

    WHERE ml.meta_message_id = p_meta_message_id AND ml.task_id IS NOT NULL

    LIMIT 1;

    END;

    $$;

    CREATE OR REPLACE FUNCTION public.log_incoming_message_rpc(

    p_message_id TEXT, p_from_number TEXT, p_timestamp TEXT,

    p_msg_type TEXT, p_body TEXT

    ) RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $$

    BEGIN

    INSERT INTO public.incoming_messages (message_id, from_number, timestamp, msg_type, body, created_at)

    VALUES (p_message_id, p_from_number, p_timestamp::BIGINT, p_msg_type, p_body, now())

    ON CONFLICT (message_id) DO NOTHING;

    END;

    $$;

    CREATE OR REPLACE FUNCTION public.update_task_status_rpc(p_task_id UUID, p_status TEXT)

    RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $$

    BEGIN UPDATE public.tasks SET status = p_status, updated_at = now() WHERE id = p_task_id; END;

    $$;

    CREATE OR REPLACE FUNCTION public.get_automation_config_rpc()

    RETURNS TABLE(meta_access_token TEXT, meta_phone_number_id TEXT, meta_waba_id TEXT)

    LANGUAGE plpgsql SECURITY DEFINER AS $$

    BEGIN RETURN QUERY SELECT ac.meta_access_token::TEXT, ac.meta_phone_number_id::TEXT, ac.meta_waba_id::TEXT FROM public.automations_config ac LIMIT 1; END;

    $$;

    CREATE OR REPLACE FUNCTION public.log_reply_to_task(

    p_meta_message_id TEXT, p_from_phone TEXT, p_response_text TEXT,

    p_parsed_status TEXT, p_task_id UUID

    ) RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $$

    BEGIN

    INSERT INTO public.incoming_messages (message_id, from_number, body, parsed_status, task_id, created_at)

    VALUES (p_meta_message_id, p_from_phone, p_response_text, p_parsed_status, p_task_id, now())

    ON CONFLICT (message_id) DO NOTHING;

    UPDATE public.tasks SET status = p_parsed_status, updated_at = now() WHERE id = p_task_id;

    END;

    $$;

    CREATE OR REPLACE FUNCTION public.get_task_title_rpc(p_task_id UUID)

    RETURNS TEXT LANGUAGE plpgsql SECURITY DEFINER AS $$

    DECLARE v_title TEXT;

    BEGIN SELECT t.title INTO v_title FROM public.tasks t WHERE t.id = p_task_id; RETURN COALESCE(v_title, 'Reunião'); END;

    $$;

    -- 4. Realtime publications

    ALTER PUBLICATION supabase_realtime ADD TABLE public.message_log;

    ALTER PUBLICATION supabase_realtime ADD TABLE public.incoming_messages;

    -- 5. Permissões

    GRANT USAGE ON SCHEMA public TO authenticated;

    GRANT ALL ON public.message_log TO authenticated;

    GRANT ALL ON public.incoming_messages TO authenticated;

    GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO authenticated;

    `

    Status atual (Jun 2026):

  • - message_log e incoming_messages tables: PENDENTE — SQL precisa ser executado no Dashboard
  • - RPCs: PENDENTE — mesmo SQL acima cria elas
  • - AppDataContext.tsx (payload.id): CORRIGIDO localmente, build feito, deploy VPS pendente
  • - Edge Functions: PRECISAM SER REDEPLOYADAS após criar tabelas/RPCs
  • Verificação pós-fix:

    `bash

    Verificar se message_log existe

    curl -s "https://dauftiqcvgaydddoxqhh.supabase.co/rest/v1/message_log?select=id&limit=1" \

    -H "Authorization: Bearer " -H "apikey: "

    deve retornar [] e não erro

    Verificar RPC

    curl -s "https://dauftiqcvgaydddoxqhh.supabase.co/rest/v1/rpc/get_task_by_meta_message_id?p_meta_message_id=test" \

    -H "Authorization: Bearer " -H "apikey: "

    deve retornar [] e não PGRST202

    `


    WhatsApp Spam Block — Causa Raiz e Prevenção

    Sintoma (Maio 2026): Número 11913412661 (Davi) bloqueado por 5 horas.

    Causa: Envio de alertas/lembretes em massa para usuários que não responderam. O WhatsApp detecta comportamento de spam quando many mensagens são enviadas sem resposta.

    Regras de warm-up para números novos:

  • - Máx 30-50 mensagens/dia para números novos/recém-conectados
  • - Pausar 24h após 3 envios consecutivos sem resposta do destinatário
  • - Não reenviar para números bloqueados por 7 dias
  • - Não enviar mensagens de alerta/lembrete para quem não interagiu recentemente
  • O que causa bloco:

  • - Enviar mensagens para listas grandes de uma vez
  • - Múltiplas tentativas para números que não respondem
  • - Mensagens promocionais/automáticas detectadas como spam
  • Prevenção:

  • - Usar o webhook de resposta (Uazapi) para confirmar engajamento antes de mais envios
  • - só disparar mensagens para números que já interagiram ou que são responsáveis por tarefas específicas
  • - Aguardar resposta antes de reenviar

  • Painel do Corretor — GraphQL API (DISCOVERED June 2025)

    The Painel has a GraphQL API at https://api.paineldocorretor.net/graphql accessible via Auth0.

    Authentication:

    `

    POST https://auth.paineldocorretor.com.br/oauth/token

    Body: {

    grant_type: "http://auth0.com/oauth/grant-type/password-realm",

    realm: "Username-Password-Authentication",

    username: "davi.freire@rochasalesseguros.com.br",

    password: "131415pi",

    client_id: "MtCJ0_REDACTED",

    audience: "https://paineldocorretor.com.br/api/",

    scope: "openid profile offline_access email",

    response_type: "token id_token"

    }

    Response: { access_token, id_token, expires_in, refresh_token }

    `

    Refresh token works: The Auth0 response includes a refresh_token. Use it with:

    `

    POST https://auth.paineldocorretor.com.br/oauth/token

    Body: { grant_type: "refresh_token", refresh_token: "...", client_id: "MtCJ0_REDACTED" }

    `

    Available fields for deal (negocio):

  • - id, nome, criadoEm, fechamento (always null), valor
  • - etapa { id, nome } — stage name
  • - contato { id, nome, email, telefones[] } — contact info
  • - produto { id, nome } — product
  • - etiquetas { nome }[] — tags
  • - anotacoes(order_by: { criadoEm: desc }, limit: 3) { mensagem, criadoEm } — recent notes
  • IMPORTANT: fechamento is always null. There is NO structured meeting date field in the Painel API. The negocio.fechamento returns null for every deal.

    Database (crm_leads_duplicate) has no meeting data either:

  • - custom_data.timeline only contains: NegocioCriado, AnotacaoRegistrada, NegocioTransferido, NegocioMovido
  • - No fields for: reunion date, reunion time, meeting type, etc.
  • For automatic meeting creation from Painel, the Painel needs to provide a webhook (n8n or direct) that fires when a meeting is created.


    CRITICAL: created_by vs responsible_id — Rayca/Maju Visibility Bug

    The Problem

    Rayca (pre-sales role) logs in but sees 0 meetings on Dashboard.

    Root causeAppDataContext.tsx lines 128-134:

    `typescript

    const tasks = useMemo(() => {

    if (userRole === 'pre_sales' && userId) {

    return allTasks.filter((t) => t.createdBy === userId);

    }

    return allTasks;

    }, [allTasks, userRole, userId]);

    `

    The filter compares t.createdBy (Supabase Auth UUID) against userId (current user). These are different UUIDs:

  • - tasks.created_by values are UAZ system UUIDs (e.g., 41539e0b-cdf2-4c7f-9751-43c855566fb0, ecb97089-3988-4a71-8ad3-36b0464d0ee7)
  • - Supabase Auth user UUIDs are completely different
  • - Result: t.createdBy === userId never matches → pre-sales always sees 0 tasks
  • Database Reality (354 tasks)

    Rayca has 7 tasks where she is responsible but 0 where she is created_by.

    User Clarification Needed

    User said: "A rayca e a Maju são pré vendedoras, pré só vê as reuniões que ele subiu para os corretores, porém em cronograma eles vem de todos os pré vendedores (reuniões subidas no geral)"

    This suggests:

    1. In Dashboard — they should see meetings they uploaded (but created_by shows 0 — all meetings have UAZ system created_by)

    2. In Cronograma — they should see all meetings from all pre-sellers

    All 354 created_by values are from UAZ system UUIDs — the original import did NOT preserve who created the meetings in UAZ. This is a data quality issue.


    REST Query 400 — Column Mismatch Pattern (CRITICAL — BREAKING BUG)

    Symptom: Supabase REST queries return 400 Bad Request in browser console, but the same query via Edge Function (service role) works fine. Session is valid (getSession result: true), but queries for specific tables always fail.

    Root cause pattern: The table schema in the database is MISSING columns that the frontend code expects.

    Example 1 — user_roles table:

  • - Frontend queries: user_roles?select=role,role_id&user_id=eq.xxx
  • - Database has: user_id, role — but NO role_id column
  • - Result: 400 Bad Request on user_roles query
  • Example 2 — clients table:

  • - Frontend queries: clients?select=id,name,cpf,cnpj,company_name,client_type,health_plan,created_at
  • - Database has: id, name, email, phone, created_at, updated_at, created_by
  • - Database is MISSING: cpf, cnpj, company_name, client_type, health_plan, owner_id
  • - Result: 400 Bad Request on clients query
  • Diagnosis command:

    `bash

    List actual columns in a table

    echo "SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'tablename' ORDER BY ordinal_position;" | supabase db query --linked

    `

    Fix: Add missing columns one by one (bulk ALTER TABLE times out on Supabase Cloud):

    `sql

    ALTER TABLE clients ADD COLUMN IF NOT EXISTS cpf TEXT;

    ALTER TABLE clients ADD COLUMN IF NOT EXISTS cnpj TEXT;

    ALTER TABLE clients ADD COLUMN IF NOT EXISTS company_name TEXT;

    ALTER TABLE clients ADD COLUMN IF NOT EXISTS client_type TEXT DEFAULT 'adesao';

    ALTER TABLE clients ADD COLUMN IF NOT EXISTS health_plan TEXT;

    ALTER TABLE clients ADD COLUMN IF NOT EXISTS owner_id UUID REFERENCES auth.users(id);

    `

    Also check RLS policies — inserts may be blocked:

    `sql

    -- Check existing policies

    SELECT policyname, cmd, roles::text FROM pg_policies WHERE tablename = 'clients' AND schemaname = 'public';

    -- Add INSERT policy if missing

    CREATE POLICY clients_insert ON public.clients FOR INSERT TO authenticated WITH CHECK (true);

    `

    Pattern: When 400 hits a table that worked before → schema drifted, new columns needed. When 400 hits a table that never worked → the Lovable-generated code expected columns that were never created in the DB.


    DocCorretor Upload — Two-Stage Flow (Server + Supabase RLS)

    Fluxo de upload no DocCorretor (clients.$id.tsx):

    1. Browser → POST /api/public/upload-document → servidor escreve arquivo no VPS ✅

    2. Browser → supabase.from("documents").insert(...) → Supabase REST API → 403 RLS

    O servidor valida o token e escreve o arquivo localmente, mas a inserção no banco é direta do navegador para o Supabase — passa pelo RLS.

    Causa do 403: O RLS documents_insert tem with_check: "true" (policy sem filtro, aceita tudo), mas o auth.uid() no contexto da API REST do Supabase pode não estar sendo passado corretamente pelo SDK supabase-js quando a conexão é feita no cliente.

    Solução: Mover a inserção no banco para o lado do servidor (via SUPABASE_SERVICE_ROLE_KEY), contornando o RLS:

    `typescript

    // upload-document.ts — após salvar o arquivo no VPS:

    const { data: ins, error: insErr } = await supabaseAdmin.from("documents").insert({...})

    // supabaseAdmin = createClient(SUPABASE_URL, SERVICE_ROLE_KEY) // bypass RLS

    `

    ES256 JWT — Validação no servidor: Tokens de Google OAuth via Supabase usam ES256 (ECDSA), não HS256. jwt.verify(token, JWT_SECRET) sempre falha com "invalid algorithm".

    `typescript

    // CORRETO — valida via API REST do Supabase:

    const validateRes = await fetch(${SB_URL}/auth/v1/user, {

    headers: { Authorization: Bearer ${token}, apikey: SB_ANON_KEY }

    });

    const userData = await validateRes.json(); // userData.id é o userId

    `

    Verificar políticas RLS via API de gestão:

    `bash

    curl -s -X POST "https://api.supabase.com/v1/projects/{REF}/database/query" \

    -H "Authorization: Bearer " \

    -H "Content-Type: application/json" \

    -d '{"query": "SELECT policyname, cmd, qual, with_check FROM pg_policies WHERE tablename = '\''documents'\'';"}'

    `

    Nota: with_check: "true" sem filtro significa policy sem restrição (aceita tudo).

    has_role funciona corretamentehas_role('107f5dde...', 'adm') retorna true paraTecrocha. O problema não é a função.


    Bug 3: Rollup Otimiza Away useMemo — Filtros Quebram Silenciosamente (CRÍTICO — Jun 2026)

    Sintoma: Gantt chart não respeita filtro de canal (channel). Todas as barras aparecem independente do filtro selecionado. O useMemo existe no código mas o valor nunca é usado.

    Causa raiz: O bundler Rollup elimina uma variável useMemo quando o resultado é passado diretamente para um componente sem ser armazenado em uma variável intermediária:

    `typescript

    // ❌ CÓDIGO QUE O ROLLUP OTIMIZA AWAY:

    const dayFilteredTasks = useMemo(() => allTasks.filter(t => / ... /), [allTasks, date]);

    // O resultado só é passado diretamente — Rollup otimiza e remove a variável

    return

    f === "all" ? dayFilteredTasks : dayFilteredTasks.filter(t => t.channel === f)

    , [dayFilteredTasks, f])} />;

    // ↑

    // O Rollup PODE otimizar isso away se não for stored

    `

    `typescript

    // ✅ CORRETO — Armazena em variável nomeada (Rollup não pode otimizar):

    const filteredTasks = useMemo(() => {

    if (channelFilter === 'all') return dayFilteredTasks;

    return dayFilteredTasks.filter(t => t.channel === channelFilter);

    }, [dayFilteredTasks, channelFilter]);

    return ;

    `

    Diagnóstico no browser:

    `js

    // Verificar se as barras existem no DOM

    document.querySelectorAll('.absolute.h-7').length

    // 0 = barras não renderizadas; >0 = existem mas podem estar escondidas

    // Verificar posição das barras (barras com left/width em % podem sobrepor)

    Array.from(document.querySelectorAll('.absolute.h-7')).map(el => ({

    left: window.getComputedStyle(el).left,

    width: window.getComputedStyle(el).width,

    bg: window.getComputedStyle(el).backgroundColor

    }))

    `

    Verificar se o bundle foi gerado corretamente:

    `bash

    Verificar se o código do filtro existe no bundle compilado

    grep -a "channelFilter" /var/www/comercialrs/dist/assets/Gantt-*.js | head -5

    Verificar se o useMemo foi preservado

    grep -a "useMemo" /var/www/comercialrs/dist/assets/Gantt-*.js | wc -l

    `

    Padrão de debug Gantt (verificado em produção, Jun 2026):

    1. browser_consoledocument.querySelectorAll('.absolute.h-7').length para contar barras

    2. Verificar posição (left, width) — se todas têm left: 37.5%, width: 37.5%, são reuniões do dia inteiro (09:00–18:00) que se sobrepõem

    3. Verificar backgroundColor — cada cor corresponde a um status diferente

    Gantt bar colors (confirmadas):

  • - rgb(168, 85, 247) = bg-purple-500 → status remarketing
  • - rgb(39, 176, 89) = bg-success → status todo ou in_progress
  • - rgb(59, 130, 246) = bg-blue → ?

  • Bug 4: Cronograma — Filtro createdBy Removido (Jun 2026)

    Decisão de design: Cronograma (Gantt) mostra TODAS as reuniões do dia para TODOS os usuários, sem filtro de createdBy. Os outros painéis (Dashboard, Kanban, Reuniões) mantêm o filtro por createdBy para não-admins.

    `typescript

    // Gantt.tsx — Removido filtro createdBy:

    // ANTES (filtradva por responsável):

    const tasks = useMemo(() => {

    const filtered = dayFilteredTasks.filter(t =>

    (!responsibleFilter || t.createdBy === responsibleFilter) &&

    (!channelFilter || t.channel === channelFilter)

    );

    return filtered;

    }, [dayFilteredTasks, responsibleFilter, channelFilter]);

    // DEPOIS (sem filtro — todos veem tudo):

    const filteredTasks = useMemo(() => {

    if (channelFilter === 'all') return dayFilteredTasks;

    return dayFilteredTasks.filter(t => t.channel === channelFilter);

    }, [dayFilteredTasks, channelFilter]);

    `

    Regras de visibilidade por painel:

    ServiceContainerHost PortInternal PortURL
    ---------------------------------------------------
    Chatwoot Railsroot-rails-1host-only3000chat.rochasalesseguros.com.br
    Chatwoot Sidekiqroot-sidekiq-1background worker
    Chatwoot Postgresroot-postgres-1host-only5432
    Chatwoot Redisroot-redis-1host-only6379
    Supabase Studio/Kongdeploy-vps-kong-130018000/8001
    UAZ CRM (Node)deploy-vps-studio-1host-only (localhost proxy)3000via nginx subdomain uazapi.rochasalesseguros.com.br
    n8nopenjawt-n8n-15678
    MongoDBrochasales-mongodb27017docker-internal
    lp-backendlp-backend80018001localhost only
    Evolution API (WhatsApp)evolution-api8080
    ConditionSyntaxExample
    ---------
    Nullfield=is.nulldeleted_at=is.null
    Not nullfield=not.is.nulldeleted_at=not.is.null
    Equalsfield=eq.valuetitle=eq.Joao
    Contains (case-insensitive)field=ilike.%25term%25title=ilike.%25CARLA%25
    Array containsfield=cs.{value}tags=cs.{"urgent"}
    created_by countMaps to
    ------
    288xUAZ system UUID — NOT Supabase auth
    66xUAZ system UUID — NOT Supabase auth
    0Rayca, Maju, or any Supabase user
    responsible_idTeam member
    ------
    117xRafael
    104xRaiany
    61xJulia
    41xVinicius
    7xRayca
    7xKeyla
    6xRaissa
    6xJulietti
    2xIlano
    2xPietra
    Paineladmin/managermember/viewer/pre_sales
    ---------
    DashboardVê tudoSó o que criou (createdBy)
    KanbanVê tudoSó o que criou
    ReuniõesVê tudoSó o que criou
    CronogramaVê tudoVê tudo (SEM filtro createdBy)


    rochasalesseguros.com.br — Edge Runtime 401 Investigation (Jun 2026)

    PROBLEMA: Admin panel sections (Ferramentas, Funções, Usuários) retornam {"error":"Unauthorized"} mesmo com token JWT válido de login.

    ARQUITETURA:

  • - rochasalesseguros.com.br (port 3000) e documentos.rochasalesseguros.com.br (port 3010) compartilham /opt/rochasales
  • - Qualquer mudança em /opt/rochasales pode afetar AMBOS apps
  • - Edge function: /opt/rochasales/supabase/functions/main/index.ts → container deploy-vps-functions-1
  • - Função principal: Deno.serveverifyTokenInlineisCallerAdmin → 401
  • O QUE FOI DESCARTADO:

    1. Kong NÃO tem plugin JWT na rota functions (só CORS plugin existe)

    2. functions-proxy nginx é proxy transparente simples (só proxy_pass)

    3. Token JWT é CRIPTOGRAFICAMENTE válido (assinatura HMAC-SHA256 confere)

    4. VERIFY_JWT=false em ambos GoTrue e edge-runtime

    5. JWT_SECRET é IGUAL em ambos (kETxz0JNJhJyg5UYxbd8zG6g2v9kXTYZGjY+3v7LUKI=)

    O QUE FALHOU:

  • - isCallerAdmin query no banco retorna 401 mesmo com SERVICE_ROLE_KEY
  • - verifyTokenInline retorna null para tokens que deveriam ser válidos
  • - Edge-runtime container NÃO tem curl, wget, ps disponíveis (só echo, cat, ls, stat)
  • - Container restart não resolveu
  • HIPÓTESES RESTANTES (não testadas):

    1. RLS nas tabelas user_roles/custom_roles bloqueia SERVICE_ROLE_KEY

    2. crypto.subtle.verify no Deno do edge-runtime falha com HS256 (espera RS256?)

    3. Header Authorization: Bearer mal-formatado chegando no edge-runtime

    4. Kong modificando headers entre o proxy e o upstream

    COMANDO ÚTIL — diff container vs local:

    `bash

    docker cp deploy-vps-functions-1:/home/deno/functions/main/index.ts /tmp/container_index.ts

    diff /tmp/container_index.ts /opt/rochasales/supabase/functions/main/index.ts

    `

    COMANDO ÚTIL — ver PID e network do container:

    `bash

    docker inspect deploy-vps-functions-1 | grep -E '"PID"|"IPAddress"|"NetworkMode"'

    `

    ATENÇÃO: Não mexer em /opt/rochasales sem antes fazer backup e confirmar que não vai quebrar DocCorretor (port 3010).


    Discord Paste Problem (recurring UX issue)

    Users copy markdown code fences ( `sql) instead of code content. Discord adds "(editado)" metadata to edited messages, breaking SQL execution.

    Always instruct: Delete the `sql lines and any "(editado)" text from pasted content before using in terminal.