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.
┌──────────────────────┐
│ 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 │
└──────────────────────┘ └──────────────────────┘
Base URL: https://admin-api.pontodomestico.com
Todos retornam JSON. Erros vem como { error: 'mensagem' }.
POST /support/tickets — abrir chamadoCria 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 completoRetorna 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:
user — o próprio usuáriobot — a Ingrid (IA)human_admin — atendente humano (admin assumiu manualmente)system — aviso automático (suspeita, erro, transferência)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.
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.ticketIdno cliente.
A persona é uma pessoa real que trabalha no atendimento do Ponto Doméstico. Regras invioláveis embarcadas no system prompt:
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.
sonnet no Claude CLI local)claude na VPS (não usa API key)Antes de chamar a IA, o backend roda a mensagem do usuário contra ~25 regex de jailbreak/injection/comandos destrutivos. Se bater:
security_flagsystem no ticket avisandoai_enabled = false) — só humano responde dali pra frenteNo painel admin (https://admin.pontodomestico.com/support), o atendente pode:
ai_enabled = true.Eventos disso chegam no app via WebSocket (support_tickets update).
POST /support/tickets: navegar pra chat. Conectar ao WS.bot/human_admin chega.user → bolha à direita, cor primáriabot → bolha à esquerda, label "Ingrid"human_admin → bolha à esquerda, label "Atendente", cor diferente (destaque suave)system → centralizado, fonte menor, fundo amareladoresolved: bloqueia input, abre form de rating.GET /support/tickets/:id pra preencher gaps.// 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;
}
}
Pra testar antes de integrar no app: https://admin-api.pontodomestico.com/chat — formulário inicial + chat completo (cria ticket guest, conecta WS, conversa).
/var/www/src/ponto-admin-api/ (Node + Express + mysql2 + socket.io)ponto-admin-api (porta 3050)ponto_domestica (container ponto-db, MySQL 8) — tabelas novas support_tickets, support_messagesadmin-api.pontodomestico.com proxy → 127.0.0.1:3050 + WebSocket upgrade/usr/bin/claude (Claude Code 2.x), OAuth do user claude/support/* — qualquer um pode abrir ticket. Se quiser amarrar a usuário logado, passe um JWT do app no body e valide server-side (não implementado).ANTHROPIC_API_KEY + --bare se latência importar.