Suporte-IA — Integração no app do Ponto Doméstico

Guia para integrar o suporte com IA (Ingrid) no app do empregador/funcionária do Ponto Doméstico. A API e o painel admin já estão prontos. Este doc descreve como o app cliente consome a API e como funciona o fluxo.

Visão geral

┌──────────────────────┐
│  App ponto (cliente) │  ← cria ticket, manda mensagens, escuta WS
└──────────┬───────────┘
           │ HTTPS + WSS
           ▼
┌──────────────────────────────────────────┐
│  admin-api.pontodomestico.com  (porta 3050) │
│  • REST /support/* (público, sem auth)   │
│  • WebSocket /socket.io namespace        │
│    /ponto-admin (eventos realtime)       │
│  • Persiste em ponto_domestica.MySQL     │
│  • Filtro regex pré-LLM (~25 padrões)    │
│  • Spawn Claude CLI (modelo sonnet)      │
└──────────────────────────────────────────┘
           │
           ▼
┌──────────────────────┐         ┌──────────────────────┐
│  Painel admin        │         │  /usr/bin/claude     │
│  admin.pontodomestico│         │  --model sonnet      │
│  /support            │         │  persona Ingrid      │
└──────────────────────┘         └──────────────────────┘

Endpoints (públicos — usados pelo app)

Base URL: https://admin-api.pontodomestico.com

Todos retornam JSON. Erros vem como { error: 'mensagem' }.

POST /support/tickets — abrir chamado

Cria um ticket e dispara a primeira resposta da IA em background.

// Request
{
  "subject": "Erro de geolocalização",
  "message": "Quando aperto o botão de bater ponto dá erro de raio.",
  "name": "Maria Silva",        // opcional — nome do usuário
  "email": "maria@example.com"  // opcional — email do usuário
}

// Response 201
{ "ticketId": 42 }

Se o usuário do app está logado (você já tem nome/email do perfil), passa esses campos. Se não, mande sem — fica como guest.

GET /support/tickets/:id — buscar ticket completo

Retorna o ticket com todas as mensagens. Útil pra carregar histórico ao abrir o chat.

{
  "ticket": {
    "id": 42,
    "subject": "Erro de geolocalização",
    "status": "open",            // open | in_progress | resolved | closed
    "rating": null,              // 1-5 ou null
    "aiEnabled": true,
    "createdAt": "2026-05-28T22:24:30.000Z",
    "user": { "name": "Maria Silva", "email": "maria@example.com" },
    "messages": [
      { "id": 1, "sender": "user", "content": "...", "createdAt": "..." },
      { "id": 2, "sender": "bot",  "content": "Oi! ...", "createdAt": "..." },
      { "id": 3, "sender": "human_admin", "content": "Aqui é a Beatriz...", "createdAt": "..." },
      { "id": 4, "sender": "system", "content": "Mensagem suspeita...", "createdAt": "..." }
    ]
  }
}

sender pode ser:

POST /support/tickets/:id/messages — enviar mensagem

// Request
{ "content": "Continuei tentando, agora aparece outro erro." }

// Response 201
{ "ok": true }

A IA é disparada em background (se aiEnabled). A resposta chega via WebSocket (não no retorno deste POST).

POST /support/tickets/:id/rating — avaliar (1-5)

// Request
{ "rating": 5 }

// Response 200
{ "ok": true }

Mostre as estrelas só quando o ticket entrar em status resolved ou closed.

WebSocket — receber respostas e atualizações em tempo real

Lib: socket.io-client.

import { Manager } from 'socket.io-client';

const manager = new Manager('https://admin-api.pontodomestico.com', {
  path: '/socket.io',
});
const socket = manager.socket('/ponto-admin');

socket.on('connect', () => console.log('[suporte] conectado'));
socket.on('event', (ev) => {
  // ev.table_obj: 'support_messages' | 'support_tickets'
  // ev.type:      'create' | 'update'
  // ev.data:      payload (depende do tipo)

  if (ev.table_obj === 'support_messages' && ev.data.ticketId === meuTicketId) {
    // Nova mensagem chegou
    appendMessage(ev.data.message);
  }
  if (ev.table_obj === 'support_tickets' && ev.data.ticketId === meuTicketId) {
    // Status mudou (ex: admin marcou como resolved)
    if (ev.data.status) updateStatus(ev.data.status);
  }
});

⚠️ Sem filtro server-side por ticket — o socket recebe todos os eventos do namespace. Filtre por ev.data.ticketId no cliente.

Comportamento da IA (Ingrid)

A persona é uma pessoa real que trabalha no atendimento do Ponto Doméstico. Regras invioláveis embarcadas no system prompt:

Mensagens fracionadas (UX)

A IA divide a resposta em 2-4 bolhas curtas usando separador ||. O backend insere cada bolha como uma mensagem separada, com delay 3-12s entre elas (proporcional ao tamanho — simula digitação humana).

Implicação pro app: depois que o usuário manda uma mensagem, fique escutando o WebSocket por ~30s. Múltiplas mensagens vão chegando uma a uma. Ideal mostrar indicador "Ingrid está digitando..." entre elas.

Modelo e custo

Filtro pré-LLM

Antes de chamar a IA, o backend roda a mensagem do usuário contra ~25 regex de jailbreak/injection/comandos destrutivos. Se bater:

  1. Marca a mensagem com security_flag
  2. Insere mensagem system no ticket avisando
  3. Desliga IA no ticket (ai_enabled = false) — só humano responde dali pra frente
  4. Painel admin recebe via WS pra revisar

Toggle IA/humano (controle do admin)

No painel admin (https://admin.pontodomestico.com/support), o atendente pode:

Eventos disso chegam no app via WebSocket (support_tickets update).

UX recomendado no app

  1. Tela inicial: form simples (assunto + mensagem inicial). Nome/email pré-preenchidos do perfil.
  2. Após POST /support/tickets: navegar pra chat. Conectar ao WS.
  3. Indicador "digitando": mostrar quando o usuário acabou de mandar mensagem; remover quando a primeira bot/human_admin chega.
  4. Bolhas de chat:
    • user → bolha à direita, cor primária
    • bot → bolha à esquerda, label "Ingrid"
    • human_admin → bolha à esquerda, label "Atendente", cor diferente (destaque suave)
    • system → centralizado, fonte menor, fundo amarelado
  5. Quando status resolved: bloqueia input, abre form de rating.
  6. Reconexão: ao perder WS, reconectar automaticamente + refetch GET /support/tickets/:id pra preencher gaps.

Exemplo mínimo (Angular service)

// support.service.ts
import { Injectable, signal } from '@angular/core';
import { Manager } from 'socket.io-client';

const BASE = 'https://admin-api.pontodomestico.com';

@Injectable({ providedIn: 'root' })
export class SupportService {
  private socket: any = null;
  ticketId = signal<number | null>(null);
  messages = signal<any[]>([]);
  status = signal<string>('open');

  async open(subject: string, message: string, name?: string, email?: string) {
    const res = await fetch(`${BASE}/support/tickets`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ subject, message, name, email }),
    });
    const { ticketId } = await res.json();
    this.ticketId.set(ticketId);
    await this.refresh();
    this.connect();
  }

  async send(content: string) {
    if (!this.ticketId()) return;
    await fetch(`${BASE}/support/tickets/${this.ticketId()}/messages`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content }),
    });
  }

  async rate(rating: number) {
    if (!this.ticketId()) return;
    await fetch(`${BASE}/support/tickets/${this.ticketId()}/rating`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ rating }),
    });
  }

  private async refresh() {
    const res = await fetch(`${BASE}/support/tickets/${this.ticketId()}`);
    const { ticket } = await res.json();
    this.messages.set(ticket.messages);
    this.status.set(ticket.status);
  }

  private connect() {
    if (this.socket) return;
    const manager = new Manager(BASE, { path: '/socket.io' });
    this.socket = manager.socket('/ponto-admin');
    this.socket.on('event', (ev: any) => {
      const tid = this.ticketId();
      if (!tid || ev.data?.ticketId !== tid) return;
      if (ev.table_obj === 'support_messages') {
        const msgs = this.messages();
        if (!msgs.some(m => m.id === ev.data.message.id)) {
          this.messages.set([...msgs, ev.data.message]);
        }
      }
      if (ev.table_obj === 'support_tickets' && ev.data.status) {
        this.status.set(ev.data.status);
      }
    });
  }

  disconnect() {
    this.socket?.disconnect();
    this.socket = null;
  }
}

Página de simulação

Pra testar antes de integrar no app: https://admin-api.pontodomestico.com/chat — formulário inicial + chat completo (cria ticket guest, conecta WS, conversa).

Stack e infra

Limitações conhecidas