fix: corrige loading infinito no login - pg.Pool + logs debug
This commit is contained in:
parent
41a9626f20
commit
0fad3e05f7
|
|
@ -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)
|
||||||
|
|
|
||||||
15
db/index.ts
15
db/index.ts
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -58,24 +58,49 @@ export function setupAuth(app: Express) {
|
||||||
|
|
||||||
passport.use(
|
passport.use(
|
||||||
new LocalStrategy(async (username, password, done) => {
|
new LocalStrategy(async (username, password, done) => {
|
||||||
|
console.log(`[Auth] Tentativa de login: ${username}`);
|
||||||
|
try {
|
||||||
const user = await storage.getUserByUsername(username);
|
const user = await storage.getUserByUsername(username);
|
||||||
if (!user || !(await comparePasswords(password, user.password))) {
|
if (!user) {
|
||||||
|
console.log(`[Auth] Usuário não encontrado: ${username}`);
|
||||||
return done(null, false);
|
return done(null, false);
|
||||||
} else {
|
}
|
||||||
|
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) => {
|
||||||
|
console.log(`[Auth] Desserializando usuário: ${id}`);
|
||||||
|
try {
|
||||||
const user = await storage.getUser(id);
|
const user = await storage.getUser(id);
|
||||||
if (user) {
|
if (user) {
|
||||||
|
console.log(`[Auth] Enriquecendo usuário: ${id}`);
|
||||||
const enrichedUser = await storage.getEnrichedUser(user);
|
const enrichedUser = await storage.getEnrichedUser(user);
|
||||||
|
console.log(`[Auth] Usuário desserializado: ${id}`);
|
||||||
done(null, enrichedUser);
|
done(null, enrichedUser);
|
||||||
} else {
|
} else {
|
||||||
|
console.log(`[Auth] Usuário não encontrado na desserialização: ${id}`);
|
||||||
done(null, null);
|
done(null, null);
|
||||||
}
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`[Auth] Erro na desserialização: ${error.message}`);
|
||||||
|
done(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/register", async (req, res, next) => {
|
app.post("/api/register", async (req, res, next) => {
|
||||||
|
|
@ -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) => {
|
||||||
|
console.log(`[Auth] Post-login, enriquecendo usuário: ${req.user!.id}`);
|
||||||
|
try {
|
||||||
const enrichedUser = await storage.getEnrichedUser(req.user!);
|
const enrichedUser = await storage.getEnrichedUser(req.user!);
|
||||||
|
console.log(`[Auth] Enviando resposta de login para: ${enrichedUser.username}`);
|
||||||
res.status(200).json(enrichedUser);
|
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) => {
|
||||||
|
|
|
||||||
|
|
@ -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,11 +307,15 @@ class LearningService {
|
||||||
} else {
|
} else {
|
||||||
errors++;
|
errors++;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
errors++;
|
errors++;
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
console.warn(`[Learning] Timeout indexing interaction ${interaction.id} (3s exceeded)`);
|
||||||
|
} else {
|
||||||
console.error(`[Learning] Error indexing interaction ${interaction.id}:`, error);
|
console.error(`[Learning] Error indexing interaction ${interaction.id}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (indexedIds.length > 0) {
|
if (indexedIds.length > 0) {
|
||||||
await this.markAsIndexed(indexedIds);
|
await this.markAsIndexed(indexedIds);
|
||||||
|
|
@ -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) {
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
console.warn("[Learning] Timeout searching similar interactions (3s exceeded)");
|
||||||
|
} else {
|
||||||
console.error("[Learning] Error searching similar interactions:", error);
|
console.error("[Learning] Error searching similar interactions:", error);
|
||||||
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue