feat: enhance DocumentacaoCard with visualizer button and improve PDF generation with structured content and metadata handling

This commit is contained in:
2025-12-07 11:49:20 -03:00
parent 426e358d86
commit 10a729baed
9 changed files with 1987 additions and 356 deletions

View File

@@ -9,6 +9,7 @@
*/
import type * as acoes from "../acoes.js";
import type * as actions_documentacaoVarredura from "../actions/documentacaoVarredura.js";
import type * as actions_email from "../actions/email.js";
import type * as actions_linkPreview from "../actions/linkPreview.js";
import type * as actions_pushNotifications from "../actions/pushNotifications.js";
@@ -100,6 +101,7 @@ import type {
declare const fullApi: ApiFromModules<{
acoes: typeof acoes;
"actions/documentacaoVarredura": typeof actions_documentacaoVarredura;
"actions/email": typeof actions_email;
"actions/linkPreview": typeof actions_linkPreview;
"actions/pushNotifications": typeof actions_pushNotifications;

View File

@@ -0,0 +1,137 @@
'use node';
import { action } from '../_generated/server';
import { v } from 'convex/values';
import { internal } from '../_generated/api';
import * as fs from 'fs';
import * as path from 'path';
/**
* Action para ler arquivos do sistema de arquivos e analisar funções
*/
export const analisarArquivos = action({
args: {
varreduraId: v.id('documentacaoVarredura'),
arquivos: v.array(v.string()),
executadoPor: v.id('usuarios')
},
returns: v.object({
arquivosAnalisados: v.number(),
documentosNovos: v.number(),
documentosAtualizados: v.number(),
erros: v.array(v.string())
}),
handler: async (ctx, args) => {
'use node';
const erros: string[] = [];
let arquivosAnalisados = 0;
let documentosNovos = 0;
let documentosAtualizados = 0;
console.log('🔍 [analisarArquivos] Iniciando análise de arquivos...');
console.log(`📁 Total de arquivos: ${args.arquivos.length}`);
console.log(`📋 Arquivos: ${args.arquivos.join(', ')}`);
// Tentar diferentes caminhos possíveis
const caminhosPossiveis = [
path.join(process.cwd(), 'packages/backend/convex'),
path.join(process.cwd(), 'convex'),
path.resolve(__dirname, '../../convex'),
path.resolve(__dirname, '../../../convex')
];
let basePath: string | null = null;
for (const caminho of caminhosPossiveis) {
if (fs.existsSync(caminho)) {
basePath = caminho;
console.log(`✅ Caminho encontrado: ${basePath}`);
break;
}
}
if (!basePath) {
const erro = `Nenhum caminho válido encontrado. Tentados: ${caminhosPossiveis.join(', ')}`;
console.error(`${erro}`);
erros.push(erro);
await ctx.runMutation(internal.documentacaoVarredura.atualizarVarreduraComResultados, {
varreduraId: args.varreduraId,
documentosNovos: 0,
documentosAtualizados: 0,
arquivosAnalisados: 0,
erros: [erro]
});
return {
arquivosAnalisados: 0,
documentosNovos: 0,
documentosAtualizados: 0,
erros: [erro]
};
}
for (const nomeArquivo of args.arquivos) {
try {
const arquivoPath = path.join(basePath, nomeArquivo);
console.log(`📄 Processando: ${arquivoPath}`);
// Verificar se o arquivo existe
if (!fs.existsSync(arquivoPath)) {
const erro = `Arquivo não encontrado: ${nomeArquivo} (caminho: ${arquivoPath})`;
console.error(`${erro}`);
erros.push(erro);
continue;
}
// Ler conteúdo do arquivo
const conteudo = fs.readFileSync(arquivoPath, 'utf-8');
console.log(`✅ Arquivo lido: ${nomeArquivo} (${conteudo.length} caracteres)`);
// Analisar funções no arquivo usando a mutation interna
const resultado = await ctx.runMutation(
internal.documentacaoVarredura.processarArquivo,
{
varreduraId: args.varreduraId,
nomeArquivo,
conteudo,
executadoPor: args.executadoPor
}
);
console.log(
`✅ Processado ${nomeArquivo}: ${resultado.documentosNovos} novos, ${resultado.documentosAtualizados} atualizados`
);
arquivosAnalisados++;
documentosNovos += resultado.documentosNovos;
documentosAtualizados += resultado.documentosAtualizados;
} catch (error) {
const erroMsg = error instanceof Error ? error.message : 'Erro desconhecido';
const erroStack = error instanceof Error ? error.stack : undefined;
const erroCompleto = `Erro ao processar ${nomeArquivo}: ${erroMsg}${erroStack ? `\n${erroStack}` : ''}`;
console.error(`${erroCompleto}`);
erros.push(erroCompleto);
}
}
console.log(
`📊 Resumo: ${arquivosAnalisados} arquivos analisados, ${documentosNovos} novos, ${documentosAtualizados} atualizados`
);
// Atualizar varredura com os resultados
await ctx.runMutation(internal.documentacaoVarredura.atualizarVarreduraComResultados, {
varreduraId: args.varreduraId,
documentosNovos,
documentosAtualizados,
arquivosAnalisados,
erros
});
return {
arquivosAnalisados,
documentosNovos,
documentosAtualizados,
erros
};
}
});

View File

@@ -1,11 +1,22 @@
import { v } from 'convex/values';
import { internalMutation, mutation } from './_generated/server';
import { internalMutation, mutation, query } from './_generated/server';
import { Doc, Id } from './_generated/dataModel';
import { internal, api } from './_generated/api';
import { getCurrentUserFunction } from './auth';
// ========== HELPERS ==========
/**
* Normaliza texto para busca (remove acentos, converte para lowercase)
*/
function normalizarTextoParaBusca(texto: string): string {
return texto
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '') // Remove diacríticos
.trim();
}
/**
* Gera hash simples de uma string (usando soma de caracteres como identificador único)
* Nota: Para produção, considere usar uma Action com Node.js para hash MD5/SHA256 real
@@ -34,6 +45,7 @@ function analisarFuncaoConvex(
parametros: string[];
retorno: string;
hash: string;
corpoFuncao: string;
}> {
const funcoes: Array<{
nome: string;
@@ -42,6 +54,7 @@ function analisarFuncaoConvex(
parametros: string[];
retorno: string;
hash: string;
corpoFuncao: string;
}> = [];
// Padrões para detectar funções
@@ -49,13 +62,13 @@ function analisarFuncaoConvex(
const padraoMutation = /export\s+const\s+(\w+)\s*=\s*mutation\s*\(/g;
const padraoAction = /export\s+const\s+(\w+)\s*=\s*action\s*\(/g;
// Extrair JSDoc comments
// Extrair JSDoc comments com melhor parsing
const padraoJSDoc = /\/\*\*([\s\S]*?)\*\//g;
const jsdocs: Map<string, string> = new Map();
const jsdocs: Map<string, { descricao: string; parametros: Map<string, string>; retorno?: string }> = new Map();
let match;
while ((match = padraoJSDoc.exec(conteudo)) !== null) {
const jsdoc = match[1].trim();
const jsdocRaw = match[1].trim();
// Tentar encontrar a função seguinte
const proximaFuncao = conteudo.indexOf('export', match.index + match[0].length);
if (proximaFuncao !== -1) {
@@ -63,7 +76,32 @@ function analisarFuncaoConvex(
.substring(proximaFuncao, proximaFuncao + 200)
.match(/export\s+const\s+(\w+)/);
if (nomeFuncao) {
jsdocs.set(nomeFuncao[1], jsdoc);
// Parse JSDoc para extrair descrição, @param, @returns
const linhas = jsdocRaw.split('\n').map((l) => l.trim().replace(/^\*\s*/, ''));
let descricao = '';
const parametros = new Map<string, string>();
let retorno: string | undefined;
for (let i = 0; i < linhas.length; i++) {
const linha = linhas[i];
if (linha.startsWith('@param')) {
const paramMatch = linha.match(/@param\s+(\w+)\s+(.+)/);
if (paramMatch) {
parametros.set(paramMatch[1], paramMatch[2]);
}
} else if (linha.startsWith('@returns') || linha.startsWith('@return')) {
retorno = linha.replace(/@returns?\s+/, '');
} else if (!linha.startsWith('@') && linha.length > 0) {
if (descricao) descricao += ' ';
descricao += linha;
}
}
jsdocs.set(nomeFuncao[1], {
descricao: descricao || jsdocRaw,
parametros,
retorno
});
}
}
}
@@ -76,28 +114,50 @@ function analisarFuncaoConvex(
const corpoFuncao = conteudo.substring(inicio, fim);
const hash = gerarHash(corpoFuncao);
// Extrair args
// Extrair args com mais detalhes
const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/);
const parametros: string[] = [];
const jsdocInfo = jsdocs.get(nome);
if (argsMatch) {
const argsContent = argsMatch[1];
const paramMatches = argsContent.matchAll(/(\w+):\s*v\.(\w+)/g);
for (const paramMatch of paramMatches) {
parametros.push(`${paramMatch[1]}: ${paramMatch[2]}`);
const paramNome = paramMatch[1];
const paramTipo = paramMatch[2];
const paramDesc = jsdocInfo?.parametros.get(paramNome);
if (paramDesc) {
parametros.push(`${paramNome}: ${paramTipo} - ${paramDesc}`);
} else {
parametros.push(`${paramNome}: ${paramTipo}`);
}
}
}
// Extrair returns
const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/);
const retorno = returnsMatch ? returnsMatch[1] : 'void';
let retorno = returnsMatch ? returnsMatch[1] : 'void';
if (jsdocInfo?.retorno) {
retorno = `${retorno} - ${jsdocInfo.retorno}`;
}
let descricao = jsdocInfo?.descricao || `Query ${nome} do arquivo ${nomeArquivo}. Esta função realiza uma consulta no banco de dados.`;
// Adicionar análise da lógica se não houver JSDoc detalhado
if (!jsdocInfo?.descricao || jsdocInfo.descricao.length < 50) {
const logica = analisarLogicaFuncao(corpoFuncao, 'query');
if (logica) {
descricao += ' ' + logica;
}
}
funcoes.push({
nome,
tipo: 'query',
descricao: jsdocs.get(nome) || `Query ${nome} do arquivo ${nomeArquivo}`,
descricao,
parametros,
retorno,
hash
hash,
corpoFuncao
});
}
@@ -112,24 +172,46 @@ function analisarFuncaoConvex(
const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/);
const parametros: string[] = [];
const jsdocInfo = jsdocs.get(nome);
if (argsMatch) {
const argsContent = argsMatch[1];
const paramMatches = argsContent.matchAll(/(\w+):\s*v\.(\w+)/g);
for (const paramMatch of paramMatches) {
parametros.push(`${paramMatch[1]}: ${paramMatch[2]}`);
const paramNome = paramMatch[1];
const paramTipo = paramMatch[2];
const paramDesc = jsdocInfo?.parametros.get(paramNome);
if (paramDesc) {
parametros.push(`${paramNome}: ${paramTipo} - ${paramDesc}`);
} else {
parametros.push(`${paramNome}: ${paramTipo}`);
}
}
}
const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/);
const retorno = returnsMatch ? returnsMatch[1] : 'void';
let retorno = returnsMatch ? returnsMatch[1] : 'void';
if (jsdocInfo?.retorno) {
retorno = `${retorno} - ${jsdocInfo.retorno}`;
}
let descricao = jsdocInfo?.descricao || `Mutation ${nome} do arquivo ${nomeArquivo}. Esta função modifica dados no banco de dados.`;
// Adicionar análise da lógica se não houver JSDoc detalhado
if (!jsdocInfo?.descricao || jsdocInfo.descricao.length < 50) {
const logica = analisarLogicaFuncao(corpoFuncao, 'mutation');
if (logica) {
descricao += ' ' + logica;
}
}
funcoes.push({
nome,
tipo: 'mutation',
descricao: jsdocs.get(nome) || `Mutation ${nome} do arquivo ${nomeArquivo}`,
descricao,
parametros,
retorno,
hash
hash,
corpoFuncao
});
}
@@ -144,30 +226,184 @@ function analisarFuncaoConvex(
const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/);
const parametros: string[] = [];
const jsdocInfo = jsdocs.get(nome);
if (argsMatch) {
const argsContent = argsMatch[1];
const paramMatches = argsContent.matchAll(/(\w+):\s*v\.(\w+)/g);
for (const paramMatch of paramMatches) {
parametros.push(`${paramMatch[1]}: ${paramMatch[2]}`);
const paramNome = paramMatch[1];
const paramTipo = paramMatch[2];
const paramDesc = jsdocInfo?.parametros.get(paramNome);
if (paramDesc) {
parametros.push(`${paramNome}: ${paramTipo} - ${paramDesc}`);
} else {
parametros.push(`${paramNome}: ${paramTipo}`);
}
}
}
const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/);
const retorno = returnsMatch ? returnsMatch[1] : 'void';
let retorno = returnsMatch ? returnsMatch[1] : 'void';
if (jsdocInfo?.retorno) {
retorno = `${retorno} - ${jsdocInfo.retorno}`;
}
let descricao = jsdocInfo?.descricao || `Action ${nome} do arquivo ${nomeArquivo}. Esta função executa ações no servidor, podendo acessar APIs externas e realizar operações assíncronas.`;
// Adicionar análise da lógica se não houver JSDoc detalhado
if (!jsdocInfo?.descricao || jsdocInfo.descricao.length < 50) {
const logica = analisarLogicaFuncao(corpoFuncao, 'action');
if (logica) {
descricao += ' ' + logica;
}
}
funcoes.push({
nome,
tipo: 'action',
descricao: jsdocs.get(nome) || `Action ${nome} do arquivo ${nomeArquivo}`,
descricao,
parametros,
retorno,
hash
hash,
corpoFuncao
});
}
return funcoes;
}
/**
* Analisa a lógica de uma função e gera uma explicação detalhada
*/
function analisarLogicaFuncao(corpoFuncao: string, tipo: 'query' | 'mutation' | 'action'): string {
const explicacoes: string[] = [];
// Detectar operações de banco de dados
const operacoesDB = {
query: corpoFuncao.match(/ctx\.db\.query\(['"]([^'"]+)['"]\)/g) || [],
insert: corpoFuncao.match(/ctx\.db\.insert\(['"]([^'"]+)['"]/g) || [],
update: corpoFuncao.match(/ctx\.db\.patch\(/g) || [],
delete: corpoFuncao.match(/ctx\.db\.delete\(/g) || [],
replace: corpoFuncao.match(/ctx\.db\.replace\(/g) || []
};
if (operacoesDB.query.length > 0) {
const tabelas = operacoesDB.query.map((q) => {
const match = q.match(/['"]([^'"]+)['"]/);
return match ? match[1] : '';
});
explicacoes.push(
`Esta função realiza consultas na(s) tabela(s): **${tabelas.filter((t) => t).join(', ')}**.`
);
}
if (operacoesDB.insert.length > 0) {
const tabelas = operacoesDB.insert.map((i) => {
const match = i.match(/['"]([^'"]+)['"]/);
return match ? match[1] : '';
});
explicacoes.push(
`A função **insere novos registros** na(s) tabela(s): **${tabelas.filter((t) => t).join(', ')}**.`
);
}
if (operacoesDB.update.length > 0) {
explicacoes.push(`A função **atualiza registros existentes** no banco de dados.`);
}
if (operacoesDB.delete.length > 0) {
explicacoes.push(`A função **remove registros** do banco de dados.`);
}
if (operacoesDB.replace.length > 0) {
explicacoes.push(`A função **substitui registros completos** no banco de dados.`);
}
// Detectar uso de índices
if (corpoFuncao.includes('.withIndex(')) {
explicacoes.push(`A função utiliza **índices do banco de dados** para otimizar as consultas.`);
}
// Detectar filtros
if (corpoFuncao.includes('.filter(')) {
explicacoes.push(`A função aplica **filtros** para restringir os resultados da consulta.`);
}
// Detectar ordenação
if (corpoFuncao.includes('.order(')) {
explicacoes.push(`A função **ordena os resultados** de acordo com critérios específicos.`);
}
// Detectar paginação
if (corpoFuncao.includes('.take(') || corpoFuncao.includes('.limit')) {
explicacoes.push(`A função implementa **paginação** para limitar a quantidade de resultados retornados.`);
}
// Detectar validações
if (corpoFuncao.includes('if (') || corpoFuncao.includes('if(')) {
const condicoes = (corpoFuncao.match(/if\s*\([^)]+\)/g) || []).length;
if (condicoes > 0) {
explicacoes.push(
`A função contém **${condicoes} condição(ões) de validação** para garantir a integridade dos dados.`
);
}
}
// Detectar loops
if (corpoFuncao.includes('for (') || corpoFuncao.includes('forEach(') || corpoFuncao.includes('.map(')) {
explicacoes.push(`A função **processa múltiplos itens** iterando sobre coleções de dados.`);
}
// Detectar chamadas a outras funções
const chamadasInternas = corpoFuncao.match(/ctx\.run(Query|Mutation|Action)\(/g) || [];
if (chamadasInternas.length > 0) {
explicacoes.push(
`A função **chama outras funções** do sistema (${chamadasInternas.length} chamada(s)) para realizar operações auxiliares.`
);
}
// Detectar tratamento de erros
if (corpoFuncao.includes('try {') || corpoFuncao.includes('catch')) {
explicacoes.push(`A função implementa **tratamento de erros** para garantir robustez.`);
}
// Detectar operações assíncronas (para actions)
if (tipo === 'action') {
if (corpoFuncao.includes('fetch(') || corpoFuncao.includes('await fetch')) {
explicacoes.push(`A função realiza **chamadas HTTP** para APIs externas.`);
}
if (corpoFuncao.includes('fs.') || corpoFuncao.includes('readFile') || corpoFuncao.includes('writeFile')) {
explicacoes.push(`A função realiza **operações de arquivo** no sistema.`);
}
}
// Detectar agregações
if (corpoFuncao.includes('.collect()')) {
explicacoes.push(`A função **coleta todos os resultados** da consulta em memória.`);
}
// Detectar contagem
if (corpoFuncao.includes('.count()') || corpoFuncao.includes('length')) {
explicacoes.push(`A função **conta elementos** ou calcula quantidades.`);
}
// Detectar transformações de dados
if (corpoFuncao.includes('.map(') || corpoFuncao.includes('.reduce(') || corpoFuncao.includes('.filter(')) {
explicacoes.push(`A função **transforma os dados** antes de retorná-los.`);
}
// Detectar autenticação/autorização
if (corpoFuncao.includes('getCurrentUser') || corpoFuncao.includes('auth') || corpoFuncao.includes('usuario')) {
explicacoes.push(`A função verifica **autenticação e autorização** do usuário.`);
}
if (explicacoes.length === 0) {
return 'A função executa operações básicas conforme sua implementação.';
}
return explicacoes.join(' ');
}
/**
* Gera conteúdo Markdown para uma função
*/
@@ -179,27 +415,77 @@ function gerarMarkdownFuncao(
parametros: string[];
retorno: string;
hash: string;
corpoFuncao: string;
},
arquivoOrigem: string
): string {
const tipoCapitalizado = funcao.tipo.charAt(0).toUpperCase() + funcao.tipo.slice(1);
let markdown = `# ${funcao.nome}\n\n`;
markdown += `## Tipo\n\n`;
markdown += `${tipoCapitalizado}\n\n`;
markdown += `## Descrição\n\n`;
markdown += `${funcao.descricao}\n\n`;
if (funcao.parametros.length > 0) {
markdown += `## Parâmetros\n\n`;
for (const param of funcao.parametros) {
markdown += `- \`${param}\`\n`;
}
markdown += `\n`;
// Descrição do tipo
let tipoDescricao = '';
switch (funcao.tipo) {
case 'query':
tipoDescricao = 'Uma **Query** é uma função de leitura que busca dados do banco de dados. Queries são otimizadas para leitura e podem ser executadas múltiplas vezes sem efeitos colaterais.';
break;
case 'mutation':
tipoDescricao = 'Uma **Mutation** é uma função de escrita que modifica dados no banco de dados. Mutations garantem consistência e podem ser executadas de forma transacional.';
break;
case 'action':
tipoDescricao = 'Uma **Action** é uma função que pode executar operações no servidor, incluindo chamadas a APIs externas, operações de arquivo e outras tarefas assíncronas.';
break;
}
markdown += `## Retorno\n\n`;
markdown += `\`${funcao.retorno}\`\n\n`;
let markdown = `# ${funcao.nome}\n\n`;
markdown += `## Visão Geral\n\n`;
markdown += `${funcao.descricao}\n\n`;
markdown += `## Tipo de Função\n\n`;
markdown += `**${tipoCapitalizado}**\n\n`;
markdown += `${tipoDescricao}\n\n`;
// Adicionar seção de lógica implementada
const logicaDetalhada = analisarLogicaFuncao(funcao.corpoFuncao, funcao.tipo);
if (logicaDetalhada && logicaDetalhada !== 'A função executa operações básicas conforme sua implementação.') {
markdown += `## Lógica Implementada\n\n`;
markdown += `${logicaDetalhada}\n\n`;
}
if (funcao.parametros.length > 0) {
markdown += `## Parâmetros de Entrada\n\n`;
markdown += `Esta função aceita os seguintes parâmetros:\n\n`;
for (const param of funcao.parametros) {
// Separar nome, tipo e descrição se houver
const paramMatch = param.match(/^(.+?):\s*(.+?)(?:\s*-\s*(.+))?$/);
if (paramMatch) {
const [, nome, tipo, desc] = paramMatch;
markdown += `### \`${nome}\`\n\n`;
markdown += `- **Tipo:** \`${tipo}\`\n`;
if (desc) {
markdown += `- **Descrição:** ${desc}\n`;
}
markdown += `\n`;
} else {
markdown += `- \`${param}\`\n`;
}
}
markdown += `\n`;
} else {
markdown += `## Parâmetros de Entrada\n\n`;
markdown += `Esta função não requer parâmetros.\n\n`;
}
markdown += `## Valor de Retorno\n\n`;
const retornoMatch = funcao.retorno.match(/^(.+?)(?:\s*-\s*(.+))?$/);
if (retornoMatch) {
const [, tipo, desc] = retornoMatch;
markdown += `- **Tipo:** \`${tipo}\`\n`;
if (desc) {
markdown += `- **Descrição:** ${desc}\n`;
}
} else {
markdown += `\`${funcao.retorno}\`\n`;
}
markdown += `\n`;
markdown += `## Arquivo Origem\n\n`;
markdown += `\`${arquivoOrigem}\`\n\n`;
@@ -286,44 +572,39 @@ export const executarVarredura = internalMutation({
}
}
// Por enquanto, vamos apenas atualizar o status da varredura
// A análise real de arquivos precisaria ser feita via Action que lê o sistema de arquivos
// ou através de um processo externo que envia os dados para o Convex
arquivosAnalisados = arquivosConvex.length;
// Atualizar status da varredura
const duracao = Date.now() - inicio;
await ctx.db.patch(varreduraId, {
status: 'concluida',
documentosEncontrados: documentosExistentes.length,
documentosNovos,
documentosAtualizados,
arquivosAnalisados,
erros: erros.length > 0 ? erros : undefined,
duracaoMs: duracao,
concluidoEm: Date.now()
// Chamar Action para ler e analisar arquivos do sistema de arquivos
// Usar scheduler para não bloquear, mas a Action atualizará a varredura quando terminar
console.log(`🚀 [executarVarredura] Iniciando análise de ${arquivosConvex.length} arquivos`);
ctx.scheduler.runAfter(
0,
api.actions.documentacaoVarredura.analisarArquivos,
{
varreduraId,
arquivos: arquivosConvex,
executadoPor: args.executadoPor
}
).catch((error) => {
console.error('❌ [executarVarredura] Erro ao agendar Action:', error);
erros.push(`Erro ao agendar análise: ${error instanceof Error ? error.message : 'Erro desconhecido'}`);
});
// Notificar TI_master se houver novos documentos
if (documentosNovos > 0) {
// Buscar IDs dos novos documentos criados durante esta varredura
const novosDocumentos = await ctx.db
.query('documentacao')
.filter((q) => q.eq(q.field('geradoAutomaticamente'), true))
.filter((q) => q.gte(q.field('criadoEm'), inicio))
.collect();
// Atualizar status inicial (a Action atualizará quando terminar)
arquivosAnalisados = arquivosConvex.length;
const duracao = Date.now() - inicio;
await ctx.db.patch(varreduraId, {
status: 'em_andamento', // Manter como em_andamento até a Action terminar
documentosEncontrados: documentosExistentes.length,
documentosNovos: 0,
documentosAtualizados: 0,
arquivosAnalisados,
erros: erros.length > 0 ? erros : undefined,
duracaoMs: duracao
});
const novosDocumentosIds = novosDocumentos.map((doc) => doc._id);
console.log(`✅ [executarVarredura] Varredura agendada, ID: ${varreduraId}`);
if (novosDocumentosIds.length > 0) {
// Notificar via scheduler para não bloquear
await ctx.scheduler.runAfter(0, api.documentacao.notificarNovosDocumentos, {
documentosIds: novosDocumentosIds,
quantidade: documentosNovos
});
}
}
// A notificação será feita pela Action quando terminar
return varreduraId;
} catch (error) {
@@ -342,6 +623,195 @@ export const executarVarredura = internalMutation({
}
});
/**
* Atualizar varredura com resultados da Action
*/
export const atualizarVarreduraComResultados = internalMutation({
args: {
varreduraId: v.id('documentacaoVarredura'),
documentosNovos: v.number(),
documentosAtualizados: v.number(),
arquivosAnalisados: v.number(),
erros: v.array(v.string())
},
handler: async (ctx, args) => {
const varredura = await ctx.db.get(args.varreduraId);
if (!varredura) return;
const duracao = Date.now() - varredura.iniciadoEm;
// Buscar documentos encontrados
const documentosEncontrados = await ctx.db
.query('documentacao')
.filter((q) => q.eq(q.field('geradoAutomaticamente'), true))
.collect();
await ctx.db.patch(args.varreduraId, {
status: 'concluida',
documentosEncontrados: documentosEncontrados.length,
documentosNovos: args.documentosNovos,
documentosAtualizados: args.documentosAtualizados,
arquivosAnalisados: args.arquivosAnalisados,
erros: args.erros.length > 0 ? args.erros : undefined,
duracaoMs: duracao,
concluidoEm: Date.now()
});
// Notificar TI_master se houver novos documentos
if (args.documentosNovos > 0) {
// Buscar IDs dos novos documentos criados durante esta varredura
const novosDocumentos = await ctx.db
.query('documentacao')
.filter((q) => q.eq(q.field('geradoAutomaticamente'), true))
.filter((q) => q.gte(q.field('criadoEm'), varredura.iniciadoEm))
.collect();
const novosDocumentosIds = novosDocumentos.map((doc) => doc._id);
if (novosDocumentosIds.length > 0) {
// Notificar via scheduler para não bloquear
await ctx.scheduler.runAfter(0, api.documentacao.notificarNovosDocumentos, {
documentosIds: novosDocumentosIds,
quantidade: args.documentosNovos
});
}
}
}
});
/**
* Processar um arquivo e criar/atualizar documentos (chamado pela Action)
*/
export const processarArquivo = internalMutation({
args: {
varreduraId: v.id('documentacaoVarredura'),
nomeArquivo: v.string(),
conteudo: v.string(),
executadoPor: v.id('usuarios')
},
returns: v.object({
documentosNovos: v.number(),
documentosAtualizados: v.number()
}),
handler: async (ctx, args) => {
let documentosNovos = 0;
let documentosAtualizados = 0;
console.log(`📝 [processarArquivo] Processando arquivo: ${args.nomeArquivo}`);
try {
// Analisar funções no arquivo
const funcoes = analisarFuncaoConvex(args.conteudo, args.nomeArquivo);
console.log(`🔍 [processarArquivo] Encontradas ${funcoes.length} funções em ${args.nomeArquivo}`);
if (funcoes.length === 0) {
console.log(`⚠️ [processarArquivo] Nenhuma função encontrada em ${args.nomeArquivo}`);
}
// Processar cada função encontrada
for (const funcao of funcoes) {
console.log(` 📌 Processando função: ${funcao.nome} (${funcao.tipo})`);
const markdown = gerarMarkdownFuncao(funcao, args.nomeArquivo);
const hashOrigem = `${args.nomeArquivo}:${funcao.hash}`;
// Verificar se já existe documento com este hash
const documentoExistente = await ctx.db
.query('documentacao')
.withIndex('by_hash_origem', (q) => q.eq('hashOrigem', hashOrigem))
.first();
const conteudoBusca = normalizarTextoParaBusca(
`${funcao.nome} ${funcao.descricao} ${markdown}`
);
if (documentoExistente) {
// Verificar se houve mudança no conteúdo (comparar hash)
const hashAtual = funcao.hash;
const hashAnterior = documentoExistente.hashOrigem?.split(':')[1] || '';
// Calcular nova versão se houver mudança
let novaVersao = documentoExistente.versao;
if (hashAtual !== hashAnterior) {
// Incrementar versão (ex: 1.0.0 -> 1.0.1, 1.0.1 -> 1.0.2, etc)
const partes = documentoExistente.versao.split('.');
const patch = parseInt(partes[2] || '0', 10) + 1;
novaVersao = `${partes[0]}.${partes[1]}.${patch}`;
console.log(
` 🔄 Função ${funcao.nome} alterada: versão ${documentoExistente.versao} -> ${novaVersao}`
);
}
// Atualizar documento existente
await ctx.db.patch(documentoExistente._id, {
titulo: `${funcao.nome} (${funcao.tipo})`,
conteudo: markdown,
conteudoBusca,
versao: novaVersao,
metadados: {
parametros: funcao.parametros,
retorno: funcao.retorno,
algoritmo: funcao.descricao
},
atualizadoEm: Date.now()
});
console.log(` ✅ Documento atualizado: ${funcao.nome} (versão ${novaVersao})`);
documentosAtualizados++;
} else {
// Criar novo documento
const agora = Date.now();
// Determinar categoria baseada no tipo
let categoriaId: Id<'documentacaoCategorias'> | undefined;
const categoriaBackend = await ctx.db
.query('documentacaoCategorias')
.filter((q) => q.eq(q.field('nome'), 'Backend'))
.first();
if (categoriaBackend) {
categoriaId = categoriaBackend._id;
}
const documentoId = await ctx.db.insert('documentacao', {
titulo: `${funcao.nome} (${funcao.tipo})`,
conteudo: markdown,
conteudoBusca,
categoriaId,
tags: [funcao.tipo, 'automatico'],
tipo: funcao.tipo,
versao: '1.0.0',
arquivoOrigem: args.nomeArquivo,
funcaoOrigem: funcao.nome,
hashOrigem: hashOrigem,
metadados: {
parametros: funcao.parametros,
retorno: funcao.retorno,
algoritmo: funcao.descricao
},
ativo: true,
criadoPor: args.executadoPor,
criadoEm: agora,
atualizadoEm: agora,
visualizacoes: 0,
geradoAutomaticamente: true
});
console.log(` ✨ Novo documento criado: ${funcao.nome} (ID: ${documentoId})`);
documentosNovos++;
}
}
} catch (error) {
const erroMsg = error instanceof Error ? error.message : 'Erro desconhecido';
console.error(`❌ [processarArquivo] Erro ao processar arquivo ${args.nomeArquivo}:`, erroMsg);
throw error;
}
console.log(
`✅ [processarArquivo] Arquivo ${args.nomeArquivo} processado: ${documentosNovos} novos, ${documentosAtualizados} atualizados`
);
return { documentosNovos, documentosAtualizados };
}
});
/**
* Criar documento a partir de função detectada
*/
@@ -469,7 +939,7 @@ export const executarVarreduraManual = mutation({
/**
* Obter histórico de varreduras
*/
export const obterHistoricoVarreduras = mutation({
export const obterHistoricoVarreduras = query({
args: {
limite: v.optional(v.number())
},
@@ -492,10 +962,56 @@ export const obterHistoricoVarreduras = mutation({
.order('desc')
.take(limite);
// Enriquecer com informações do usuário
// Enriquecer com informações do usuário e arquivos modificados
const varredurasEnriquecidas = await Promise.all(
varreduras.map(async (varredura) => {
const usuario = await ctx.db.get(varredura.executadoPor);
// Buscar documentos atualizados durante esta varredura
const documentosAtualizados = await ctx.db
.query('documentacao')
.filter((q) => q.eq(q.field('geradoAutomaticamente'), true))
.filter((q) => q.gte(q.field('atualizadoEm'), varredura.iniciadoEm))
.filter((q) => q.lte(q.field('atualizadoEm'), varredura.concluidoEm || Date.now()))
.collect();
// Agrupar por arquivo e obter versões
const arquivosModificados = new Map<string, { versao: string; funcoes: string[] }>();
for (const doc of documentosAtualizados) {
if (doc.arquivoOrigem) {
const arquivo = doc.arquivoOrigem;
if (!arquivosModificados.has(arquivo)) {
arquivosModificados.set(arquivo, { versao: doc.versao, funcoes: [] });
}
const info = arquivosModificados.get(arquivo)!;
if (doc.funcaoOrigem) {
info.funcoes.push(doc.funcaoOrigem);
}
// Manter a versão mais recente (comparar numericamente)
const partesAtual = doc.versao.split('.').map(Number);
const partesInfo = info.versao.split('.').map(Number);
let versaoMaisNova = info.versao;
for (let i = 0; i < Math.max(partesAtual.length, partesInfo.length); i++) {
const atual = partesAtual[i] || 0;
const infoParte = partesInfo[i] || 0;
if (atual > infoParte) {
versaoMaisNova = doc.versao;
break;
} else if (atual < infoParte) {
break;
}
}
info.versao = versaoMaisNova;
}
}
const arquivosModificadosArray = Array.from(arquivosModificados.entries()).map(([arquivo, info]) => ({
arquivo,
versao: info.versao,
funcoes: info.funcoes
}));
return {
...varredura,
executadoPorUsuario: usuario
@@ -504,7 +1020,9 @@ export const obterHistoricoVarreduras = mutation({
nome: usuario.nome,
email: usuario.email
}
: null
: null,
arquivosModificados: arquivosModificadosArray,
totalArquivosModificados: arquivosModificados.size
};
})
);