feat: add marked library for markdown parsing and enhance documentation handling with new cron job for scheduled checks

This commit is contained in:
2025-12-06 20:43:41 -03:00
parent f3b4721119
commit 0ec12721ba
17 changed files with 3033 additions and 4 deletions

View File

@@ -0,0 +1,634 @@
import { v } from 'convex/values';
import { internalMutation, mutation } from './_generated/server';
import { Doc, Id } from './_generated/dataModel';
import { internal, api } from './_generated/api';
import { getCurrentUserFunction } from './auth';
// ========== HELPERS ==========
/**
* 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
*/
function gerarHash(texto: string): string {
// Hash simples baseado em soma de códigos de caracteres
let hash = 0;
for (let i = 0; i < texto.length; i++) {
const char = texto.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32bit integer
}
return Math.abs(hash).toString(16);
}
/**
* Extrai informações de uma função Convex (query, mutation, action)
*/
function analisarFuncaoConvex(
conteudo: string,
nomeArquivo: string
): Array<{
nome: string;
tipo: 'query' | 'mutation' | 'action';
descricao: string;
parametros: string[];
retorno: string;
hash: string;
}> {
const funcoes: Array<{
nome: string;
tipo: 'query' | 'mutation' | 'action';
descricao: string;
parametros: string[];
retorno: string;
hash: string;
}> = [];
// Padrões para detectar funções
const padraoQuery = /export\s+const\s+(\w+)\s*=\s*query\s*\(/g;
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
const padraoJSDoc = /\/\*\*([\s\S]*?)\*\//g;
const jsdocs: Map<string, string> = new Map();
let match;
while ((match = padraoJSDoc.exec(conteudo)) !== null) {
const jsdoc = match[1].trim();
// Tentar encontrar a função seguinte
const proximaFuncao = conteudo.indexOf('export', match.index + match[0].length);
if (proximaFuncao !== -1) {
const nomeFuncao = conteudo
.substring(proximaFuncao, proximaFuncao + 200)
.match(/export\s+const\s+(\w+)/);
if (nomeFuncao) {
jsdocs.set(nomeFuncao[1], jsdoc);
}
}
}
// Buscar queries
while ((match = padraoQuery.exec(conteudo)) !== null) {
const nome = match[1];
const inicio = match.index;
const fim = conteudo.indexOf('});', inicio) + 3;
const corpoFuncao = conteudo.substring(inicio, fim);
const hash = gerarHash(corpoFuncao);
// Extrair args
const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/);
const parametros: string[] = [];
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]}`);
}
}
// Extrair returns
const returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/);
const retorno = returnsMatch ? returnsMatch[1] : 'void';
funcoes.push({
nome,
tipo: 'query',
descricao: jsdocs.get(nome) || `Query ${nome} do arquivo ${nomeArquivo}`,
parametros,
retorno,
hash
});
}
// Buscar mutations
padraoMutation.lastIndex = 0;
while ((match = padraoMutation.exec(conteudo)) !== null) {
const nome = match[1];
const inicio = match.index;
const fim = conteudo.indexOf('});', inicio) + 3;
const corpoFuncao = conteudo.substring(inicio, fim);
const hash = gerarHash(corpoFuncao);
const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/);
const parametros: string[] = [];
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 returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/);
const retorno = returnsMatch ? returnsMatch[1] : 'void';
funcoes.push({
nome,
tipo: 'mutation',
descricao: jsdocs.get(nome) || `Mutation ${nome} do arquivo ${nomeArquivo}`,
parametros,
retorno,
hash
});
}
// Buscar actions
padraoAction.lastIndex = 0;
while ((match = padraoAction.exec(conteudo)) !== null) {
const nome = match[1];
const inicio = match.index;
const fim = conteudo.indexOf('});', inicio) + 3;
const corpoFuncao = conteudo.substring(inicio, fim);
const hash = gerarHash(corpoFuncao);
const argsMatch = corpoFuncao.match(/args:\s*\{([\s\S]*?)\}/);
const parametros: string[] = [];
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 returnsMatch = corpoFuncao.match(/returns:\s*v\.(\w+)/);
const retorno = returnsMatch ? returnsMatch[1] : 'void';
funcoes.push({
nome,
tipo: 'action',
descricao: jsdocs.get(nome) || `Action ${nome} do arquivo ${nomeArquivo}`,
parametros,
retorno,
hash
});
}
return funcoes;
}
/**
* Gera conteúdo Markdown para uma função
*/
function gerarMarkdownFuncao(
funcao: {
nome: string;
tipo: 'query' | 'mutation' | 'action';
descricao: string;
parametros: string[];
retorno: string;
hash: 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`;
}
markdown += `## Retorno\n\n`;
markdown += `\`${funcao.retorno}\`\n\n`;
markdown += `## Arquivo Origem\n\n`;
markdown += `\`${arquivoOrigem}\`\n\n`;
markdown += `## Hash\n\n`;
markdown += `\`${funcao.hash}\`\n\n`;
return markdown;
}
// ========== INTERNAL MUTATIONS ==========
/**
* Executar varredura automática (chamado por cron ou manualmente)
*/
export const executarVarredura = internalMutation({
args: {
executadoPor: v.id('usuarios'),
tipo: v.union(v.literal('automatica'), v.literal('manual'))
},
handler: async (ctx, args) => {
const inicio = Date.now();
const varreduraId = await ctx.db.insert('documentacaoVarredura', {
tipo: args.tipo,
status: 'em_andamento',
documentosEncontrados: 0,
documentosNovos: 0,
documentosAtualizados: 0,
arquivosAnalisados: 0,
executadoPor: args.executadoPor,
iniciadoEm: inicio
});
const erros: string[] = [];
let documentosNovos = 0;
let documentosAtualizados = 0;
let arquivosAnalisados = 0;
try {
// Lista de arquivos conhecidos para análise
// Nota: Em produção, isso poderia ser expandido para ler do sistema de arquivos via Action
const arquivosConvex = [
'usuarios.ts',
'funcionarios.ts',
'chat.ts',
'email.ts',
'pontos.ts',
'ferias.ts',
'ausencias.ts',
'chamados.ts',
'pedidos.ts',
'produtos.ts',
'flows.ts',
'contratos.ts',
'empresas.ts',
'setores.ts',
'times.ts',
'cursos.ts',
'lgpd.ts',
'security.ts',
'monitoramento.ts',
'config.ts',
'configuracaoEmail.ts',
'configuracaoPonto.ts',
'configuracaoRelogio.ts',
'configuracaoJitsi.ts'
];
// Para cada arquivo conhecido, buscar funções já documentadas
// e comparar com o que deveria existir
// Como não podemos ler arquivos diretamente, vamos usar uma abordagem diferente:
// Vamos criar documentos baseados em padrões conhecidos do sistema
// Buscar todas as funções documentadas existentes
const documentosExistentes = await ctx.db
.query('documentacao')
.filter((q) => q.eq(q.field('geradoAutomaticamente'), true))
.collect();
const hashDocumentosExistentes = new Map<string, Id<'documentacao'>>();
for (const doc of documentosExistentes) {
if (doc.hashOrigem) {
hashDocumentosExistentes.set(doc.hashOrigem, doc._id);
}
}
// 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()
});
// 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();
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: documentosNovos
});
}
}
return varreduraId;
} catch (error) {
const duracao = Date.now() - inicio;
erros.push(error instanceof Error ? error.message : 'Erro desconhecido');
await ctx.db.patch(varreduraId, {
status: 'erro',
erros,
duracaoMs: duracao,
concluidoEm: Date.now()
});
throw error;
}
}
});
/**
* Criar documento a partir de função detectada
*/
export const criarDocumentoFuncao = internalMutation({
args: {
titulo: v.string(),
conteudo: v.string(),
tipo: v.union(
v.literal('query'),
v.literal('mutation'),
v.literal('action'),
v.literal('component'),
v.literal('route'),
v.literal('modulo'),
v.literal('manual'),
v.literal('outro')
),
arquivoOrigem: v.string(),
funcaoOrigem: v.string(),
hashOrigem: v.string(),
metadados: v.optional(
v.object({
parametros: v.optional(v.array(v.string())),
retorno: v.optional(v.string()),
dependencias: v.optional(v.array(v.string())),
exemplos: v.optional(v.array(v.string())),
algoritmo: v.optional(v.string())
})
),
criadoPor: v.id('usuarios')
},
handler: async (ctx, args) => {
// Verificar se já existe documento com este hash
const documentoExistente = await ctx.db
.query('documentacao')
.withIndex('by_hash_origem', (q) => q.eq('hashOrigem', args.hashOrigem))
.first();
if (documentoExistente) {
// Atualizar documento existente
const conteudoBusca = args.titulo.toLowerCase() + ' ' + args.conteudo.toLowerCase();
await ctx.db.patch(documentoExistente._id, {
titulo: args.titulo,
conteudo: args.conteudo,
conteudoBusca,
metadados: args.metadados,
atualizadoEm: Date.now()
});
return { documentoId: documentoExistente._id, novo: false };
}
// Criar novo documento
const agora = Date.now();
const conteudoBusca = args.titulo.toLowerCase() + ' ' + args.conteudo.toLowerCase();
// 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: args.titulo,
conteudo: args.conteudo,
conteudoBusca,
categoriaId,
tags: [args.tipo, 'automatico'],
tipo: args.tipo,
versao: '1.0.0',
arquivoOrigem: args.arquivoOrigem,
funcaoOrigem: args.funcaoOrigem,
hashOrigem: args.hashOrigem,
metadados: args.metadados,
ativo: true,
criadoPor: args.criadoPor,
criadoEm: agora,
atualizadoEm: agora,
visualizacoes: 0,
geradoAutomaticamente: true
});
return { documentoId, novo: true };
}
});
// ========== PUBLIC MUTATIONS ==========
/**
* Executar varredura manualmente (chamado pelo frontend)
*/
export const executarVarreduraManual = mutation({
args: {},
handler: async (ctx) => {
const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
throw new Error('Não autenticado');
}
// Verificar se é TI
const role = await ctx.db.get(usuarioAtual.roleId);
if (!role || (role.nivel > 1 && role.nome !== 'ti_master' && role.nome !== 'ti_usuario')) {
throw new Error('Acesso negado. Apenas usuários TI podem executar varredura.');
}
// Executar varredura
const varreduraId = await ctx.scheduler.runAfter(
0,
internal.documentacaoVarredura.executarVarredura,
{
executadoPor: usuarioAtual._id,
tipo: 'manual'
}
);
return varreduraId;
}
});
/**
* Obter histórico de varreduras
*/
export const obterHistoricoVarreduras = mutation({
args: {
limite: v.optional(v.number())
},
handler: async (ctx, args) => {
const usuarioAtual = await getCurrentUserFunction(ctx);
if (!usuarioAtual) {
throw new Error('Não autenticado');
}
// Verificar se é TI
const role = await ctx.db.get(usuarioAtual.roleId);
if (!role || (role.nivel > 1 && role.nome !== 'ti_master' && role.nome !== 'ti_usuario')) {
throw new Error('Acesso negado.');
}
const limite = args.limite || 20;
const varreduras = await ctx.db
.query('documentacaoVarredura')
.withIndex('by_iniciado_em', (q) => q.gte('iniciadoEm', 0))
.order('desc')
.take(limite);
// Enriquecer com informações do usuário
const varredurasEnriquecidas = await Promise.all(
varreduras.map(async (varredura) => {
const usuario = await ctx.db.get(varredura.executadoPor);
return {
...varredura,
executadoPorUsuario: usuario
? {
_id: usuario._id,
nome: usuario.nome,
email: usuario.email
}
: null
};
})
);
return varredurasEnriquecidas;
}
});
/**
* Verificar e executar varredura agendada (chamado pelo cron)
*/
export const verificarEExecutarVarreduraAgendada = internalMutation({
args: {},
handler: async (ctx) => {
// Buscar configuração ativa
const config = await ctx.db
.query('documentacaoConfig')
.withIndex('by_ativo', (q) => q.eq('ativo', true))
.first();
if (!config) {
return; // Nenhuma configuração ativa
}
const agora = new Date();
const diaSemanaAtual = agora.getDay(); // 0 = domingo, 1 = segunda, etc.
const diasSemanaMap: Record<
'domingo' | 'segunda' | 'terca' | 'quarta' | 'quinta' | 'sexta' | 'sabado',
number
> = {
domingo: 0,
segunda: 1,
terca: 2,
quarta: 3,
quinta: 4,
sexta: 5,
sabado: 6
};
// Verificar se hoje é um dos dias configurados
const diaAtualNome = Object.keys(diasSemanaMap).find(
(key) => diasSemanaMap[key as keyof typeof diasSemanaMap] === diaSemanaAtual
) as 'domingo' | 'segunda' | 'terca' | 'quarta' | 'quinta' | 'sexta' | 'sabado' | undefined;
if (!diaAtualNome || !config.diasSemana.includes(diaAtualNome)) {
return; // Hoje não é um dia configurado
}
// Verificar se é o horário configurado (com tolerância de 5 minutos)
const [horaConfig, minutoConfig] = config.horario.split(':').map(Number);
const horaAtual = agora.getHours();
const minutoAtual = agora.getMinutes();
const diferencaMinutos = horaAtual * 60 + minutoAtual - (horaConfig * 60 + minutoConfig);
if (Math.abs(diferencaMinutos) > 5) {
return; // Não é o horário configurado
}
// Verificar se já foi executado hoje neste horário
const hojeInicio = new Date(agora);
hojeInicio.setHours(0, 0, 0, 0);
const hojeFim = new Date(agora);
hojeFim.setHours(23, 59, 59, 999);
const varredurasHoje = await ctx.db
.query('documentacaoVarredura')
.filter((q) =>
q.and(
q.gte(q.field('iniciadoEm'), hojeInicio.getTime()),
q.lte(q.field('iniciadoEm'), hojeFim.getTime()),
q.eq(q.field('tipo'), 'automatica')
)
)
.collect();
// Se já foi executado hoje neste horário, não executar novamente
if (varredurasHoje.length > 0) {
const ultimaVarredura = varredurasHoje.sort((a, b) => b.iniciadoEm - a.iniciadoEm)[0];
const ultimaVarreduraData = new Date(ultimaVarredura.iniciadoEm);
const ultimaVarreduraHora = ultimaVarreduraData.getHours();
const ultimaVarreduraMinuto = ultimaVarreduraData.getMinutes();
if (
ultimaVarreduraHora === horaConfig &&
Math.abs(ultimaVarreduraMinuto - minutoConfig) <= 5
) {
return; // Já foi executado hoje neste horário
}
}
// Buscar um usuário TI_master para executar a varredura
const roleTIMaster = await ctx.db
.query('roles')
.filter((q) => q.eq(q.field('nivel'), 0))
.first();
if (!roleTIMaster) {
return; // Não há TI_master configurado
}
const usuarioTIMaster = await ctx.db
.query('usuarios')
.filter((q) => q.eq(q.field('roleId'), roleTIMaster._id))
.first();
if (!usuarioTIMaster) {
return; // Não há usuário TI_master
}
// Executar varredura
await ctx.scheduler.runAfter(
0,
internal.documentacaoVarredura.executarVarredura,
{
executadoPor: usuarioTIMaster._id,
tipo: 'automatica'
}
);
// Atualizar próxima execução na configuração
await ctx.db.patch(config._id, {
ultimaExecucao: Date.now()
});
}
});