diff --git a/backend/src/application/jobs/processar_ranking.py b/backend/src/application/jobs/processar_ranking.py index e8e5ece..6eb1c44 100644 --- a/backend/src/application/jobs/processar_ranking.py +++ b/backend/src/application/jobs/processar_ranking.py @@ -51,7 +51,7 @@ class ProcessarRankingJob: job_status.finalizar(sucesso=True) - return { + resultado_final = { "sucesso": True, "total_processados": resultado.get("processados", len(entries)), "total_batches": resultado.get("batches", 0), @@ -59,9 +59,15 @@ class ProcessarRankingJob: "estatisticas": estatisticas } + self._consultores = [] + + return resultado_final + except Exception as e: + self._consultores = [] job_status.finalizar(sucesso=False, erro=str(e)) - raise RuntimeError(f"Erro ao processar ranking: {e}") + logger.error(f"Erro ao processar ranking: {e}", exc_info=True) + raise RuntimeError(f"Erro ao processar ranking: {e}") from e async def _processar_batch(self, docs: list, progress: dict) -> None: for doc in docs: diff --git a/backend/src/domain/services/calculador_pontuacao.py b/backend/src/domain/services/calculador_pontuacao.py index 2a1f4b1..27dcce1 100644 --- a/backend/src/domain/services/calculador_pontuacao.py +++ b/backend/src/domain/services/calculador_pontuacao.py @@ -91,9 +91,10 @@ class CalculadorPontuacao: bonus = 0 # Bônus de continuidade (escalonado, não cumulativo) - apenas CONS_ATIVO + # Spec: 8+ anos = 20 pts (era 15, corrigido conforme especificação) if codigo == "CONS_ATIVO": if consultoria.anos_consecutivos >= 8: - bonus += 15 + bonus += 20 elif consultoria.anos_consecutivos >= 5: bonus += 10 elif consultoria.anos_consecutivos >= 3: diff --git a/backend/src/infrastructure/elasticsearch/client.py b/backend/src/infrastructure/elasticsearch/client.py index 09e1fa5..ec90a50 100644 --- a/backend/src/infrastructure/elasticsearch/client.py +++ b/backend/src/infrastructure/elasticsearch/client.py @@ -30,7 +30,12 @@ class ElasticsearchClient: "Accept": "application/json" }, verify=self.verify_ssl, - timeout=120.0 + timeout=httpx.Timeout( + connect=10.0, + read=60.0, + write=30.0, + pool=10.0, + ) ) async def close(self) -> None: @@ -39,8 +44,8 @@ class ElasticsearchClient: @property def client(self) -> httpx.AsyncClient: - if not self._client: - raise RuntimeError("Cliente Elasticsearch não conectado. Execute connect() primeiro.") + if not self._client or self._client.is_closed: + raise RuntimeError("Cliente Elasticsearch não conectado ou fechado. Execute connect() primeiro.") return self._client async def buscar_por_id(self, id_pessoa: int) -> Optional[dict]: diff --git a/backend/src/infrastructure/oracle/client.py b/backend/src/infrastructure/oracle/client.py index 240ffc7..536b29f 100644 --- a/backend/src/infrastructure/oracle/client.py +++ b/backend/src/infrastructure/oracle/client.py @@ -21,13 +21,20 @@ class OracleClient: user=self.user, password=self.password, dsn=self.dsn, - min=2, - max=10, - increment=1, + min=1, + max=20, + increment=5, + timeout=30, + wait_timeout=10, + getmode=oracledb.POOL_GETMODE_TIMEDWAIT, ) self._connected = True + logger.info("Pool Oracle conectado com sucesso") + except oracledb.Error as e: + logger.error(f"Oracle database error: {e}", exc_info=True) + self._connected = False except Exception as e: - logger.warning(f"Oracle: {e}") + logger.error(f"Oracle connection error: {e}", exc_info=True) self._connected = False def close(self) -> None: @@ -45,11 +52,13 @@ class OracleClient: def get_connection(self): if not self._pool: raise RuntimeError("Pool Oracle não conectado. Execute connect() primeiro.") - conn = self._pool.acquire() + conn = None try: + conn = self._pool.acquire() yield conn finally: - self._pool.release(conn) + if conn: + self._pool.release(conn) def executar_query(self, query: str, params: Optional[dict] = None) -> List[Dict[str, Any]]: if not self.is_connected: diff --git a/backend/src/infrastructure/oracle/ranking_repository.py b/backend/src/infrastructure/oracle/ranking_repository.py index 7493853..412378a 100644 --- a/backend/src/infrastructure/oracle/ranking_repository.py +++ b/backend/src/infrastructure/oracle/ranking_repository.py @@ -99,11 +99,19 @@ class RankingOracleRepository: """ Busca ranking paginado ordenado por posição. """ + if page < 1: + page = 1 + if size < 1 or size > 10000: + size = 50 + offset = (page - 1) * size limit_end = offset + size where_clause = "" - params = {} + params = { + "offset": offset, + "limit_end": limit_end, + } if filtro_ativo is not None: where_clause = "AND ATIVO = :ativo" @@ -128,7 +136,7 @@ class RankingOracleRepository: FROM TB_RANKING_CONSULTOR WHERE 1=1 {where_clause} ) - WHERE RN > {offset} AND RN <= {limit_end} + WHERE RN > :offset AND RN <= :limit_end """ results = self.client.executar_query(query, params) diff --git a/backend/src/infrastructure/repositories/consultor_repository_impl.py b/backend/src/infrastructure/repositories/consultor_repository_impl.py index 3ce680b..3a67474 100644 --- a/backend/src/infrastructure/repositories/consultor_repository_impl.py +++ b/backend/src/infrastructure/repositories/consultor_repository_impl.py @@ -63,6 +63,10 @@ class ConsultorRepositoryImpl(ConsultorRepository): return "CAJ_MP" elif "adjunt" in texto: return "CAJ" + elif "coordenador" in texto or "área" in texto or "area" in texto: + return "CA" + + logger.warning(f"Tipo de coordenação desconhecido, inferindo CA: tipo='{tipo_coord}', texto='{texto[:100]}'") return "CA" def _extrair_coordenacoes_capes(self, atuacoes: List[Dict[str, Any]]) -> List[CoordenacaoCapes]: diff --git a/backend/src/interface/api/app.py b/backend/src/interface/api/app.py index 6045ca9..073bd1b 100644 --- a/backend/src/interface/api/app.py +++ b/backend/src/interface/api/app.py @@ -48,7 +48,7 @@ async def carregar_ranking_do_oracle() -> int: for c in consultores: try: detalhes = json.loads(c.json_detalhes) if isinstance(c.json_detalhes, str) else c.json_detalhes or {} - except: + except (json.JSONDecodeError, TypeError, ValueError): detalhes = {} entries.append( @@ -120,13 +120,13 @@ async def lifespan(app: FastAPI): if scheduler: try: scheduler.parar() - except: + except Exception: pass if oracle_client: try: oracle_client.close() - except: + except Exception: pass await es_client.close() diff --git a/backend/src/interface/api/dependencies.py b/backend/src/interface/api/dependencies.py index 8c69ef1..ca958da 100644 --- a/backend/src/interface/api/dependencies.py +++ b/backend/src/interface/api/dependencies.py @@ -1,3 +1,6 @@ +import threading +from typing import Optional + from ...infrastructure.elasticsearch.client import ElasticsearchClient from ...infrastructure.repositories.consultor_repository_impl import ConsultorRepositoryImpl from ...infrastructure.oracle.client import OracleClient @@ -15,22 +18,25 @@ es_client = ElasticsearchClient( verify_ssl=settings.ES_VERIFY_SSL, ) -oracle_client = OracleClient( +oracle_client: Optional[OracleClient] = OracleClient( user=settings.ORACLE_LOCAL_USER, password=settings.ORACLE_LOCAL_PASSWORD, dsn=settings.ORACLE_LOCAL_DSN, ) if settings.ORACLE_LOCAL_USER and settings.ORACLE_LOCAL_DSN else None -ranking_oracle_repo = RankingOracleRepository(oracle_client) if oracle_client else None +ranking_oracle_repo: Optional[RankingOracleRepository] = RankingOracleRepository(oracle_client) if oracle_client else None -_repository: ConsultorRepositoryImpl = None -_processar_job: ProcessarRankingJob = None +_repository: Optional[ConsultorRepositoryImpl] = None +_processar_job: Optional[ProcessarRankingJob] = None +_lock = threading.Lock() def get_repository() -> ConsultorRepositoryImpl: global _repository if _repository is None: - _repository = ConsultorRepositoryImpl(es_client=es_client, oracle_client=oracle_client) + with _lock: + if _repository is None: + _repository = ConsultorRepositoryImpl(es_client=es_client, oracle_client=oracle_client) return _repository @@ -49,9 +55,11 @@ def get_ranking_oracle_repo() -> RankingOracleRepository: def get_processar_job() -> ProcessarRankingJob: global _processar_job if _processar_job is None: - _processar_job = ProcessarRankingJob( - es_client=es_client, - ranking_store=ranking_store, - ranking_oracle_repo=ranking_oracle_repo, - ) + with _lock: + if _processar_job is None: + _processar_job = ProcessarRankingJob( + es_client=es_client, + ranking_store=ranking_store, + ranking_oracle_repo=ranking_oracle_repo, + ) return _processar_job diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8987132..bc7d09a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -72,7 +72,11 @@ function App() { setProcessMessage('Processamento do ranking já iniciado. Aguardando...'); } - while (true) { + const MAX_POLLING_TIME = 5 * 60 * 1000; + const POLLING_INTERVAL = 4000; + const startTime = Date.now(); + + while (Date.now() - startTime < MAX_POLLING_TIME) { try { const st = await rankingService.getStatus(); setProcessMessage(st.mensagem || `Processando... ${st.progress || 0}%`); @@ -83,7 +87,11 @@ function App() { } catch (e) { setProcessMessage('Aguardando status do processamento...'); } - await new Promise((r) => setTimeout(r, 4000)); + await new Promise((r) => setTimeout(r, POLLING_INTERVAL)); + } + + if (Date.now() - startTime >= MAX_POLLING_TIME) { + throw new Error('Timeout: processamento demorou mais que 5 minutos'); } const response = await rankingService.getRanking(page, pageSize); diff --git a/frontend/src/components/ConsultorCard.jsx b/frontend/src/components/ConsultorCard.jsx index d1c592e..605ff09 100644 --- a/frontend/src/components/ConsultorCard.jsx +++ b/frontend/src/components/ConsultorCard.jsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, useMemo, memo } from 'react'; import './ConsultorCard.css'; const SELOS = { @@ -123,7 +123,7 @@ const FORMULAS = { }, bloco_c: { titulo: 'Consultoria', - descricao: 'CONS_ATIVO=150 | CONS_HIST=100 | CONS_FALECIDO=100\nTempo: 5 pts/ano (max 50)\nContinuidade 8a+=+20 (escalonado)\nRetorno (reativação): +15 (uma vez)', + descricao: 'CONS_ATIVO=150 | CONS_HIST=100 | CONS_FALECIDO=100\nTempo: 5 pts/ano (max 50)\nContinuidade: 3a=+5, 5a=+10, 8a+=+20 (escalonado)\nRetorno (reativação): +15 (uma vez)', }, bloco_d: { titulo: 'Premiacoes/Avaliacoes', @@ -183,15 +183,16 @@ const ScoreItemWithTooltip = ({ value, label, formula, style }) => ( ); -const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado }) => { +const ConsultorCard = memo(({ consultor, highlight, selecionado, onToggleSelecionado }) => { const [expanded, setExpanded] = useState(false); const cardRef = useRef(null); useEffect(() => { if (highlight && cardRef.current) { - setTimeout(() => { + const timeoutId = setTimeout(() => { cardRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 100); + return () => clearTimeout(timeoutId); } }, [highlight]); @@ -219,7 +220,7 @@ const ConsultorCard = ({ consultor, highlight, selecionado, onToggleSelecionado const blocoD = pontuacao?.bloco_d || { total: consultor.bloco_d || 0 }; const pontuacaoTotal = (blocoA.total || 0) + (blocoB.total || 0) + (blocoC.total || 0) + (blocoD.total || 0); - const selos = gerarSelos(consultor); + const selos = useMemo(() => gerarSelos(consultor), [consultor]); return (