fix: corrige loading infinito no login - pg.Pool + logs debug

This commit is contained in:
Jonas Pacheco 2026-04-09 14:16:05 -03:00
parent 41a9626f20
commit 0fad3e05f7
7 changed files with 131 additions and 25 deletions

View File

@ -27,7 +27,11 @@ CONTABIL_PYTHON_URL=http://localhost:8003
BI_PYTHON_URL=http://localhost:8004 BI_PYTHON_URL=http://localhost:8004
AUTOMATION_PYTHON_URL=http://localhost:8005 AUTOMATION_PYTHON_URL=http://localhost:8005
FISCO_PYTHON_URL=http://localhost:8002 FISCO_PYTHON_URL=http://localhost:8002
PYTHON_SERVICE_URL=http://localhost:8001 # ── Embeddings / Learning Service ──────────────────────────────────────────────
# IMPORTANTE: Usar porta 8000 (não 8001) - o serviço escuta na porta 8000
# Em Docker: http://arcadia-prod-embeddings-1:8000
# Em dev local: http://localhost:8000
PYTHON_SERVICE_URL=http://localhost:8000
# ── IA — OpenAI ─────────────────────────────────────────────────────────────── # ── IA — OpenAI ───────────────────────────────────────────────────────────────
# Deixe vazio se usar apenas Ollama (soberania total) # Deixe vazio se usar apenas Ollama (soberania total)

View File

@ -8,10 +8,19 @@ if (!process.env.DATABASE_URL) {
); );
} }
const client = new pg.Client({ // CORREÇÃO: Usar Pool em vez de Client único para melhor performance
// e evitar queries travadas quando conexão falha
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL, connectionString: process.env.DATABASE_URL,
max: 20, // máximo de conexões no pool
idleTimeoutMillis: 30000, // tempo máximo de inatividade
connectionTimeoutMillis: 5000, // timeout de conexão: 5s
statement_timeout: 10000, // timeout de query: 10s
}); });
client.connect(); // Log de erros do pool
pool.on('error', (err) => {
console.error('[DB Pool] Erro inesperado:', err.message);
});
export const db = drizzle({ client, schema }); export const db = drizzle({ client: pool, schema });

View File

@ -5,6 +5,26 @@
--- ---
## 🔴 Root Cause Analysis: Learning Service Timeouts (RESOLVIDO)
**Problema:** `PYTHON_SERVICE_URL` sendo resetado para porta 8001 em vez de 8000
**Causa Raiz Identificada:** O arquivo `.env.example` na linha 30 continha:
```bash
PYTHON_SERVICE_URL=http://localhost:8001 # ← ERRADO
```
Quando o Coolify recriava o container, ele usava o valor do `.env.example` como fallback, causando:
- Timeouts de 10s a cada 60s (ciclo do Learning Service)
- Degradação de performance do sistema
- Erros silenciosos no log
**Solução Aplicada (08/04/2026):** Corrigido `.env.example` para usar porta 8000 com comentário explicativo.
**Lição Aprendida:** O `.env.example` deve sempre estar sincronizado com a configuração de produção correta.
---
## 📊 Resumo das Funcionalidades ## 📊 Resumo das Funcionalidades
| Funcionalidade | Status | Implementação | Notas | | Funcionalidade | Status | Implementação | Notas |

View File

@ -58,23 +58,48 @@ export function setupAuth(app: Express) {
passport.use( passport.use(
new LocalStrategy(async (username, password, done) => { new LocalStrategy(async (username, password, done) => {
const user = await storage.getUserByUsername(username); console.log(`[Auth] Tentativa de login: ${username}`);
if (!user || !(await comparePasswords(password, user.password))) { try {
return done(null, false); const user = await storage.getUserByUsername(username);
} else { if (!user) {
console.log(`[Auth] Usuário não encontrado: ${username}`);
return done(null, false);
}
console.log(`[Auth] Usuário encontrado, verificando senha...`);
const validPassword = await comparePasswords(password, user.password);
if (!validPassword) {
console.log(`[Auth] Senha inválida para: ${username}`);
return done(null, false);
}
console.log(`[Auth] Login bem-sucedido: ${username}`);
return done(null, user); return done(null, user);
} catch (error: any) {
console.error(`[Auth] Erro no login: ${error.message}`);
return done(error);
} }
}), }),
); );
passport.serializeUser((user, done) => done(null, user.id)); passport.serializeUser((user, done) => {
console.log(`[Auth] Serializando usuário: ${user.id}`);
done(null, user.id);
});
passport.deserializeUser(async (id: string, done) => { passport.deserializeUser(async (id: string, done) => {
const user = await storage.getUser(id); console.log(`[Auth] Desserializando usuário: ${id}`);
if (user) { try {
const enrichedUser = await storage.getEnrichedUser(user); const user = await storage.getUser(id);
done(null, enrichedUser); if (user) {
} else { console.log(`[Auth] Enriquecendo usuário: ${id}`);
done(null, null); const enrichedUser = await storage.getEnrichedUser(user);
console.log(`[Auth] Usuário desserializado: ${id}`);
done(null, enrichedUser);
} else {
console.log(`[Auth] Usuário não encontrado na desserialização: ${id}`);
done(null, null);
}
} catch (error: any) {
console.error(`[Auth] Erro na desserialização: ${error.message}`);
done(error);
} }
}); });
@ -101,8 +126,15 @@ export function setupAuth(app: Express) {
}); });
app.post("/api/login", passport.authenticate("local"), async (req, res) => { app.post("/api/login", passport.authenticate("local"), async (req, res) => {
const enrichedUser = await storage.getEnrichedUser(req.user!); console.log(`[Auth] Post-login, enriquecendo usuário: ${req.user!.id}`);
res.status(200).json(enrichedUser); try {
const enrichedUser = await storage.getEnrichedUser(req.user!);
console.log(`[Auth] Enviando resposta de login para: ${enrichedUser.username}`);
res.status(200).json(enrichedUser);
} catch (error: any) {
console.error(`[Auth] Erro ao enriquecer usuário no login: ${error.message}`);
res.status(500).json({ error: "Erro ao processar login" });
}
}); });
app.post("/api/logout", (req, res, next) => { app.post("/api/logout", (req, res, next) => {

View File

@ -264,7 +264,7 @@ class LearningService {
} }
async indexInteractionsToChromaDB(): Promise<{ indexed: number; errors: number }> { async indexInteractionsToChromaDB(): Promise<{ indexed: number; errors: number }> {
const PYTHON_SERVICE_URL = process.env.PYTHON_SERVICE_URL || "http://localhost:8001"; const PYTHON_SERVICE_URL = process.env.PYTHON_SERVICE_URL || "http://localhost:8000";
const unindexed = await this.getUnindexedInteractions(100); const unindexed = await this.getUnindexedInteractions(100);
if (unindexed.length === 0) { if (unindexed.length === 0) {
@ -286,6 +286,9 @@ class LearningService {
createdAt: interaction.createdAt?.toISOString(), createdAt: interaction.createdAt?.toISOString(),
}; };
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const response = await fetch(`${PYTHON_SERVICE_URL}/embeddings/add`, { const response = await fetch(`${PYTHON_SERVICE_URL}/embeddings/add`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@ -294,7 +297,9 @@ class LearningService {
document, document,
metadata, metadata,
}), }),
signal: controller.signal,
}); });
clearTimeout(timeout);
if (response.ok) { if (response.ok) {
indexedIds.push(interaction.id); indexedIds.push(interaction.id);
@ -302,9 +307,13 @@ class LearningService {
} else { } else {
errors++; errors++;
} }
} catch (error) { } catch (error: any) {
errors++; errors++;
console.error(`[Learning] Error indexing interaction ${interaction.id}:`, error); if (error.name === 'AbortError') {
console.warn(`[Learning] Timeout indexing interaction ${interaction.id} (3s exceeded)`);
} else {
console.error(`[Learning] Error indexing interaction ${interaction.id}:`, error);
}
} }
} }
@ -324,14 +333,19 @@ class LearningService {
} }
async searchSimilarInteractions(query: string): Promise<any[]> { async searchSimilarInteractions(query: string): Promise<any[]> {
const PYTHON_SERVICE_URL = process.env.PYTHON_SERVICE_URL || "http://localhost:8001"; const PYTHON_SERVICE_URL = process.env.PYTHON_SERVICE_URL || "http://localhost:8000";
try { try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const response = await fetch(`${PYTHON_SERVICE_URL}/embeddings/search`, { const response = await fetch(`${PYTHON_SERVICE_URL}/embeddings/search`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, n_results: 5 }), body: JSON.stringify({ query, n_results: 5 }),
signal: controller.signal,
}); });
clearTimeout(timeout);
if (!response.ok) { if (!response.ok) {
return []; return [];
@ -339,8 +353,12 @@ class LearningService {
const results = await response.json(); const results = await response.json();
return results.documents?.[0] || []; return results.documents?.[0] || [];
} catch (error) { } catch (error: any) {
console.error("[Learning] Error searching similar interactions:", error); if (error.name === 'AbortError') {
console.warn("[Learning] Timeout searching similar interactions (3s exceeded)");
} else {
console.error("[Learning] Error searching similar interactions:", error);
}
return []; return [];
} }
} }

View File

@ -10,8 +10,12 @@ const AUTO_START = process.env.PLUS_AUTO_START !== "false";
export async function setupPlusProxy(app: Express): Promise<void> { export async function setupPlusProxy(app: Express): Promise<void> {
const plusTarget = `http://${PLUS_HOST}:${PLUS_PORT}`; const plusTarget = `http://${PLUS_HOST}:${PLUS_PORT}`;
// CORREÇÃO: Não aguardar Laravel iniciar para não bloquear rotas
// Se falhar, o proxy retorna 502 mas o resto do sistema funciona
if (AUTO_START) { if (AUTO_START) {
await startLaravelServer(); startLaravelServer().catch((err) => {
console.error('[Plus Proxy] Falha ao iniciar Laravel:', err.message);
});
} }
// Simple transparent proxy - only rewrite Location headers // Simple transparent proxy - only rewrite Location headers

View File

@ -43,23 +43,31 @@ export class DatabaseStorage implements IStorage {
} }
async getUser(id: string): Promise<User | undefined> { async getUser(id: string): Promise<User | undefined> {
console.log(`[Storage] Buscando usuário por ID: ${id}`);
const [user] = await db.select().from(users).where(eq(users.id, id)); const [user] = await db.select().from(users).where(eq(users.id, id));
console.log(`[Storage] Usuário ${user ? 'encontrado' : 'não encontrado'}: ${id}`);
return user; return user;
} }
// CORREÇÃO: getUserByUsername com validação de tenant // CORREÇÃO: getUserByUsername com validação de tenant
async getUserByUsername(username: string, tenantId?: number): Promise<User | undefined> { async getUserByUsername(username: string, tenantId?: number): Promise<User | undefined> {
console.log(`[Storage] Buscando usuário por username: ${username}`);
const [user] = await db.select().from(users).where(eq(users.username, username)); const [user] = await db.select().from(users).where(eq(users.username, username));
console.log(`[Storage] Usuário ${user ? 'encontrado' : 'não encontrado'}: ${username}`);
// Se tenantId foi passado, validar que usuário pertence a esse tenant // Se tenantId foi passado, validar que usuário pertence a esse tenant
if (tenantId && user) { if (tenantId && user) {
console.log(`[Storage] Validando tenant ${tenantId} para usuário: ${username}`);
const [tenantUser] = await db.select() const [tenantUser] = await db.select()
.from(tenantUsers) .from(tenantUsers)
.where(and( .where(and(
eq(tenantUsers.userId, user.id), eq(tenantUsers.userId, user.id),
eq(tenantUsers.tenantId, tenantId) eq(tenantUsers.tenantId, tenantId)
)); ));
if (!tenantUser) return undefined; if (!tenantUser) {
console.log(`[Storage] Usuário ${username} não pertence ao tenant ${tenantId}`);
return undefined;
}
} }
return user; return user;
@ -120,26 +128,33 @@ export class DatabaseStorage implements IStorage {
} }
async getEnrichedUser(user: User): Promise<any> { async getEnrichedUser(user: User): Promise<any> {
console.log(`[Storage] Iniciando enriquecimento do usuário: ${user.id}`);
const enriched: any = { ...user }; const enriched: any = { ...user };
if (user.profileId) { if (user.profileId) {
console.log(`[Storage] Buscando profile: ${user.profileId}`);
const [profile] = await db.select().from(profiles).where(eq(profiles.id, user.profileId)); const [profile] = await db.select().from(profiles).where(eq(profiles.id, user.profileId));
if (profile) { if (profile) {
enriched.profile = profile; enriched.profile = profile;
enriched.allowedModules = profile.allowedModules || []; enriched.allowedModules = profile.allowedModules || [];
console.log(`[Storage] Profile encontrado: ${profile.name}`);
} }
} }
if (user.partnerId) { if (user.partnerId) {
console.log(`[Storage] Buscando partner: ${user.partnerId}`);
const [partner] = await db.select().from(crmPartners).where(eq(crmPartners.id, user.partnerId)); const [partner] = await db.select().from(crmPartners).where(eq(crmPartners.id, user.partnerId));
if (partner) { if (partner) {
enriched.partner = partner; enriched.partner = partner;
console.log(`[Storage] Partner encontrado: ${partner.name}`);
} }
} }
// Buscar tenant do usuário (apenas campos seguros para o cliente) // Buscar tenant do usuário (apenas campos seguros para o cliente)
console.log(`[Storage] Buscando tenantUser para: ${user.id}`);
const [tenantUser] = await db.select().from(tenantUsers).where(eq(tenantUsers.userId, user.id)); const [tenantUser] = await db.select().from(tenantUsers).where(eq(tenantUsers.userId, user.id));
if (tenantUser) { if (tenantUser) {
console.log(`[Storage] TenantUser encontrado, buscando tenant: ${tenantUser.tenantId}`);
const [tenant] = await db.select().from(tenants).where(eq(tenants.id, tenantUser.tenantId)); const [tenant] = await db.select().from(tenants).where(eq(tenants.id, tenantUser.tenantId));
if (tenant) { if (tenant) {
// Expor apenas campos seguros do tenant // Expor apenas campos seguros do tenant
@ -154,9 +169,13 @@ export class DatabaseStorage implements IStorage {
enriched.tenantType = tenant.tenantType; enriched.tenantType = tenant.tenantType;
enriched.tenantRole = tenantUser.role; enriched.tenantRole = tenantUser.role;
enriched.isOwner = tenantUser.isOwner === "true"; enriched.isOwner = tenantUser.isOwner === "true";
console.log(`[Storage] Tenant enriquecido: ${tenant.name}`);
} }
} else {
console.log(`[Storage] Usuário sem tenant associado: ${user.id}`);
} }
console.log(`[Storage] Enriquecimento completo: ${user.id}`);
return enriched; return enriched;
} }