Spaces:
Sleeping
Sleeping
| import { Injectable } from '@nestjs/common'; | |
| import { InjectModel } from '@nestjs/mongoose'; | |
| import { Model } from 'mongoose'; | |
| import { AccessLog } from './entities/access-log.entity'; | |
| import { ErrorLog } from './entities/error-log.entity'; | |
| import { Musica } from '../musica/entities/musica.entity'; | |
| import { FeedbackAprendizado } from '../musica/entities/feedback-aprendizado.entity'; | |
| import { LearningEvent } from '../musica/entities/learning-event.entity'; | |
| import { DecisionTrace } from '../musica/entities/decision-trace.entity'; | |
| import { AdaptivePrior } from '../musica/entities/adaptive-prior.entity'; | |
| interface RegistrarAcessoInput { | |
| method: string; | |
| path: string; | |
| statusCode: number; | |
| durationMs: number; | |
| ip?: string; | |
| userAgent?: string; | |
| metadata?: Record<string, unknown>; | |
| } | |
| interface RegistrarErroInput { | |
| method: string; | |
| path: string; | |
| statusCode: number; | |
| message?: string; | |
| errorName?: string; | |
| stack?: string; | |
| ip?: string; | |
| userAgent?: string; | |
| requestContext?: Record<string, unknown>; | |
| } | |
| interface DashboardFiltro { | |
| range?: string; | |
| dias?: number; | |
| horas?: number; | |
| } | |
| interface PeriodoResolvido { | |
| inicio: Date; | |
| fim: Date; | |
| janelaHoras: number; | |
| range: string; | |
| descricao: string; | |
| bucket: 'hora' | 'dia'; | |
| } | |
| () | |
| export class AdminService { | |
| constructor( | |
| (AccessLog.name) | |
| private readonly accessLogModel: Model<AccessLog>, | |
| (ErrorLog.name) | |
| private readonly errorLogModel: Model<ErrorLog>, | |
| (Musica.name) | |
| private readonly musicaModel: Model<Musica>, | |
| (FeedbackAprendizado.name) | |
| private readonly feedbackModel: Model<FeedbackAprendizado>, | |
| (LearningEvent.name) | |
| private readonly learningEventModel: Model<LearningEvent>, | |
| (DecisionTrace.name) | |
| private readonly decisionTraceModel: Model<DecisionTrace>, | |
| (AdaptivePrior.name) | |
| private readonly adaptivePriorModel: Model<AdaptivePrior>, | |
| ) {} | |
| validarChaveDashboard(chaveInformada: string | undefined): boolean { | |
| const chaveEsperada = process.env.DASHBOARD_KEY || 'desenrolaai-dashboard-2026'; | |
| return !!chaveInformada && chaveInformada.trim() === chaveEsperada; | |
| } | |
| async registrarAcesso(input: RegistrarAcessoInput): Promise<void> { | |
| await this.accessLogModel.create(input); | |
| } | |
| async registrarErro(input: RegistrarErroInput): Promise<void> { | |
| await this.errorLogModel.create(input); | |
| } | |
| async obterDashboard(filtro: DashboardFiltro): Promise<Record<string, unknown>> { | |
| const periodo = this.resolverPeriodo(filtro); | |
| const matchPeriodo = { createdAt: { $gte: periodo.inicio, $lte: periodo.fim } }; | |
| const [ | |
| totalAcessos, | |
| acessosPeriodo, | |
| ipsUnicosPeriodo, | |
| ipsProcessamentoUnicos, | |
| processamentoResumo, | |
| processamentosPorInstrumento, | |
| processamentosPorDificuldade, | |
| processamentosPorOrigem, | |
| endpointsMaisAcessados, | |
| tendencia, | |
| fonteResultados, | |
| topMusicas, | |
| totalErrosPeriodo, | |
| resumoErrosPeriodo, | |
| errosPorTipo, | |
| errosRecentes, | |
| retencaoPorIp, | |
| processamentosPorIp, | |
| totalFeedbackPeriodo, | |
| feedbackAcertos, | |
| feedbackCorrecoesTop, | |
| feedbackPrecisaoPorInstrumento, | |
| learningResumo, | |
| learningPorEvento, | |
| learningPorInstrumento, | |
| learningResumoLive, | |
| decisionTraceResumo, | |
| decisionTracePorInstrumento, | |
| adaptivePriorsRecentes, | |
| ] = await Promise.all([ | |
| this.accessLogModel.countDocuments(), | |
| this.accessLogModel.countDocuments(matchPeriodo), | |
| this.accessLogModel.distinct('ip', { ...matchPeriodo, ip: { $ne: null } }), | |
| this.accessLogModel.distinct('ip', { | |
| ...matchPeriodo, | |
| ip: { $ne: null }, | |
| path: '/api/musica/processar', | |
| }), | |
| this.accessLogModel.aggregate([ | |
| { $match: { ...matchPeriodo, path: '/api/musica/processar' } }, | |
| { | |
| $group: { | |
| _id: null, | |
| total: { $sum: 1 }, | |
| sucessos: { | |
| $sum: { | |
| $cond: [{ $lt: ['$statusCode', 400] }, 1, 0], | |
| }, | |
| }, | |
| falhas: { | |
| $sum: { | |
| $cond: [{ $gte: ['$statusCode', 400] }, 1, 0], | |
| }, | |
| }, | |
| tempoMedioMs: { $avg: '$durationMs' }, | |
| }, | |
| }, | |
| ]), | |
| this.accessLogModel.aggregate([ | |
| { $match: { ...matchPeriodo, path: '/api/musica/processar' } }, | |
| { | |
| $group: { | |
| _id: '$metadata.instrumento', | |
| total: { $sum: 1 }, | |
| falhas: { | |
| $sum: { | |
| $cond: [{ $gte: ['$statusCode', 400] }, 1, 0], | |
| }, | |
| }, | |
| }, | |
| }, | |
| { $sort: { total: -1 } }, | |
| ]), | |
| this.accessLogModel.aggregate([ | |
| { $match: { ...matchPeriodo, path: '/api/musica/processar' } }, | |
| { | |
| $group: { | |
| _id: '$metadata.dificuldade', | |
| total: { $sum: 1 }, | |
| }, | |
| }, | |
| { $sort: { total: -1 } }, | |
| ]), | |
| this.accessLogModel.aggregate([ | |
| { $match: { ...matchPeriodo, path: '/api/musica/processar' } }, | |
| { | |
| $group: { | |
| _id: { | |
| usouAudio: '$metadata.usouAudio', | |
| usouTexto: '$metadata.usouTexto', | |
| }, | |
| total: { $sum: 1 }, | |
| }, | |
| }, | |
| { $sort: { total: -1 } }, | |
| ]), | |
| this.accessLogModel.aggregate([ | |
| { $match: matchPeriodo }, | |
| { | |
| $group: { | |
| _id: '$path', | |
| total: { $sum: 1 }, | |
| erros: { | |
| $sum: { | |
| $cond: [{ $gte: ['$statusCode', 400] }, 1, 0], | |
| }, | |
| }, | |
| tempoMedioMs: { $avg: '$durationMs' }, | |
| }, | |
| }, | |
| { $sort: { total: -1 } }, | |
| { $limit: 12 }, | |
| ]), | |
| this.accessLogModel.aggregate([ | |
| { $match: matchPeriodo }, | |
| { | |
| $group: { | |
| _id: { | |
| $dateToString: { | |
| format: periodo.bucket === 'hora' ? '%Y-%m-%d %H:00' : '%Y-%m-%d', | |
| date: '$createdAt', | |
| }, | |
| }, | |
| total: { $sum: 1 }, | |
| erros: { | |
| $sum: { | |
| $cond: [{ $gte: ['$statusCode', 400] }, 1, 0], | |
| }, | |
| }, | |
| }, | |
| }, | |
| { $sort: { _id: 1 } }, | |
| ]), | |
| this.musicaModel.aggregate([ | |
| { $match: matchPeriodo }, | |
| { $group: { _id: '$fonte', total: { $sum: 1 } } }, | |
| { $sort: { total: -1 } }, | |
| ]), | |
| this.musicaModel.aggregate([ | |
| { $match: matchPeriodo }, | |
| { | |
| $group: { | |
| _id: { | |
| titulo: { $ifNull: ['$titulo', 'Sem titulo'] }, | |
| artista: { $ifNull: ['$artista', 'Sem artista'] }, | |
| }, | |
| total: { $sum: 1 }, | |
| }, | |
| }, | |
| { $sort: { total: -1 } }, | |
| { $limit: 12 }, | |
| ]), | |
| this.errorLogModel.countDocuments(matchPeriodo), | |
| this.errorLogModel.aggregate([ | |
| { $match: matchPeriodo }, | |
| { | |
| $group: { | |
| _id: null, | |
| total: { $sum: 1 }, | |
| operacionais: { | |
| $sum: { | |
| $cond: [{ $gte: ['$statusCode', 500] }, 1, 0], | |
| }, | |
| }, | |
| cliente: { | |
| $sum: { | |
| $cond: [ | |
| { | |
| $and: [ | |
| { $gte: ['$statusCode', 400] }, | |
| { $lt: ['$statusCode', 500] }, | |
| ], | |
| }, | |
| 1, | |
| 0, | |
| ], | |
| }, | |
| }, | |
| adminAuth: { | |
| $sum: { | |
| $cond: [ | |
| { | |
| $and: [ | |
| { $in: ['$statusCode', [401, 403]] }, | |
| { $eq: ['$path', '/api/admin/dashboard'] }, | |
| ], | |
| }, | |
| 1, | |
| 0, | |
| ], | |
| }, | |
| }, | |
| }, | |
| }, | |
| ]), | |
| this.errorLogModel.aggregate([ | |
| { $match: matchPeriodo }, | |
| { | |
| $group: { | |
| _id: '$errorName', | |
| total: { $sum: 1 }, | |
| }, | |
| }, | |
| { $sort: { total: -1 } }, | |
| ]), | |
| this.errorLogModel | |
| .find({ createdAt: { $gte: periodo.inicio, $lte: periodo.fim } }, { stack: 0 }) | |
| .sort({ createdAt: -1 }) | |
| .limit(30) | |
| .lean(), | |
| this.accessLogModel.aggregate([ | |
| { $match: { ...matchPeriodo, ip: { $ne: null } } }, | |
| { | |
| $group: { | |
| _id: { | |
| ip: '$ip', | |
| dia: { | |
| $dateToString: { | |
| format: '%Y-%m-%d', | |
| date: '$createdAt', | |
| }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| { | |
| $group: { | |
| _id: '$_id.ip', | |
| diasAtivos: { $sum: 1 }, | |
| }, | |
| }, | |
| ]), | |
| this.accessLogModel.aggregate([ | |
| { | |
| $match: { | |
| ...matchPeriodo, | |
| ip: { $ne: null }, | |
| path: '/api/musica/processar', | |
| statusCode: { $lt: 400 }, | |
| }, | |
| }, | |
| { | |
| $group: { | |
| _id: '$ip', | |
| total: { $sum: 1 }, | |
| }, | |
| }, | |
| { $sort: { total: -1 } }, | |
| { $limit: 15 }, | |
| ]), | |
| this.feedbackModel.countDocuments(matchPeriodo), | |
| this.feedbackModel.countDocuments({ | |
| ...matchPeriodo, | |
| acertou: true, | |
| }), | |
| this.feedbackModel.aggregate([ | |
| { | |
| $match: { | |
| ...matchPeriodo, | |
| acertou: false, | |
| tomCorreto: { $ne: null }, | |
| }, | |
| }, | |
| { | |
| $group: { | |
| _id: { | |
| instrumento: '$instrumento', | |
| previsto: '$tomPrevisto', | |
| correto: '$tomCorreto', | |
| }, | |
| total: { $sum: 1 }, | |
| }, | |
| }, | |
| { $sort: { total: -1 } }, | |
| { $limit: 20 }, | |
| ]), | |
| this.feedbackModel.aggregate([ | |
| { | |
| $match: { | |
| ...matchPeriodo, | |
| instrumento: { $ne: null }, | |
| }, | |
| }, | |
| { | |
| $group: { | |
| _id: '$instrumento', | |
| total: { $sum: 1 }, | |
| acertos: { | |
| $sum: { | |
| $cond: ['$acertou', 1, 0], | |
| }, | |
| }, | |
| }, | |
| }, | |
| { $sort: { total: -1 } }, | |
| ]), | |
| this.learningEventModel.aggregate([ | |
| { $match: matchPeriodo }, | |
| { | |
| $group: { | |
| _id: null, | |
| total: { $sum: 1 }, | |
| explicitos: { | |
| $sum: { $cond: [{ $eq: ['$signalKind', 'explicit'] }, 1, 0] }, | |
| }, | |
| implicitos: { | |
| $sum: { $cond: [{ $eq: ['$signalKind', 'implicit'] }, 1, 0] }, | |
| }, | |
| sistemicos: { | |
| $sum: { $cond: [{ $eq: ['$signalKind', 'system'] }, 1, 0] }, | |
| }, | |
| processCompleted: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'process_completed'] }, 1, 0] }, | |
| }, | |
| resultViewed: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'result_viewed'] }, 1, 0] }, | |
| }, | |
| confirmations: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'confirmation'] }, 1, 0] }, | |
| }, | |
| corrections: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'correction'] }, 1, 0] }, | |
| }, | |
| partialCorrections: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'partial_correction'] }, 1, 0] }, | |
| }, | |
| quickAbandons: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'quick_abandon'] }, 1, 0] }, | |
| }, | |
| reprocesses: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'reprocess'] }, 1, 0] }, | |
| }, | |
| instrumentChanges: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'instrument_change'] }, 1, 0] }, | |
| }, | |
| transpositions: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'transpose_usage'] }, 1, 0] }, | |
| }, | |
| stageUses: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'stage_mode'] }, 1, 0] }, | |
| }, | |
| liveSessionSummaries: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'live_session_summary'] }, 1, 0] }, | |
| }, | |
| }, | |
| }, | |
| ]), | |
| this.learningEventModel.aggregate([ | |
| { $match: matchPeriodo }, | |
| { | |
| $group: { | |
| _id: '$eventType', | |
| total: { $sum: 1 }, | |
| }, | |
| }, | |
| { $sort: { total: -1 } }, | |
| ]), | |
| this.learningEventModel.aggregate([ | |
| { | |
| $match: { | |
| ...matchPeriodo, | |
| instrumento: { $ne: null }, | |
| }, | |
| }, | |
| { | |
| $group: { | |
| _id: '$instrumento', | |
| total: { $sum: 1 }, | |
| corrections: { | |
| $sum: { | |
| $cond: [{ $in: ['$eventType', ['correction', 'partial_correction']] }, 1, 0], | |
| }, | |
| }, | |
| quickAbandons: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'quick_abandon'] }, 1, 0] }, | |
| }, | |
| reprocesses: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'reprocess'] }, 1, 0] }, | |
| }, | |
| liveSessionSummaries: { | |
| $sum: { $cond: [{ $eq: ['$eventType', 'live_session_summary'] }, 1, 0] }, | |
| }, | |
| avgFirstUsefulMs: { | |
| $avg: { | |
| $cond: [ | |
| { $eq: ['$eventType', 'live_session_summary'] }, | |
| '$metricas.firstUsefulMs', | |
| null, | |
| ], | |
| }, | |
| }, | |
| avgLatencyMs: { | |
| $avg: { | |
| $cond: [ | |
| { $eq: ['$eventType', 'live_session_summary'] }, | |
| '$metricas.avgLatencyMs', | |
| null, | |
| ], | |
| }, | |
| }, | |
| avgFallbacks: { | |
| $avg: { | |
| $cond: [ | |
| { $eq: ['$eventType', 'live_session_summary'] }, | |
| '$metricas.fallbackCount', | |
| null, | |
| ], | |
| }, | |
| }, | |
| }, | |
| }, | |
| { $sort: { total: -1 } }, | |
| ]), | |
| this.learningEventModel.aggregate([ | |
| { | |
| $match: { | |
| ...matchPeriodo, | |
| eventType: 'live_session_summary', | |
| }, | |
| }, | |
| { | |
| $group: { | |
| _id: null, | |
| total: { $sum: 1 }, | |
| avgDurationMs: { $avg: '$metricas.durationMs' }, | |
| avgFirstUsefulMs: { $avg: '$metricas.firstUsefulMs' }, | |
| avgLatencyMs: { $avg: '$metricas.avgLatencyMs' }, | |
| avgFallbacks: { $avg: '$metricas.fallbackCount' }, | |
| avgAmbiguityMoments: { $avg: '$metricas.ambiguityMoments' }, | |
| }, | |
| }, | |
| ]), | |
| this.decisionTraceModel.aggregate([ | |
| { $match: matchPeriodo }, | |
| { | |
| $group: { | |
| _id: null, | |
| total: { $sum: 1 }, | |
| memoriaAplicada: { | |
| $sum: { | |
| $cond: [{ $eq: ['$memoryDecision.applied', true] }, 1, 0], | |
| }, | |
| }, | |
| comPerfilAdaptativo: { | |
| $sum: { | |
| $cond: [{ $ne: ['$adaptiveProfile', null] }, 1, 0], | |
| }, | |
| }, | |
| comQuaseVitoria: { | |
| $sum: { | |
| $cond: [{ $gt: [{ $size: { $ifNull: ['$almostWon', []] } }, 0] }, 1, 0], | |
| }, | |
| }, | |
| }, | |
| }, | |
| ]), | |
| this.decisionTraceModel.aggregate([ | |
| { | |
| $match: { | |
| ...matchPeriodo, | |
| instrumento: { $ne: null }, | |
| }, | |
| }, | |
| { | |
| $group: { | |
| _id: '$instrumento', | |
| total: { $sum: 1 }, | |
| memoriaAplicada: { | |
| $sum: { | |
| $cond: [{ $eq: ['$memoryDecision.applied', true] }, 1, 0], | |
| }, | |
| }, | |
| }, | |
| }, | |
| { $sort: { total: -1 } }, | |
| ]), | |
| this.adaptivePriorModel | |
| .find({ lastEventAt: { $gte: periodo.inicio, $lte: periodo.fim } }) | |
| .sort({ lastEventAt: -1 }) | |
| .limit(12) | |
| .lean(), | |
| ]); | |
| const resumoProc = (processamentoResumo[0] || { | |
| total: 0, | |
| sucessos: 0, | |
| falhas: 0, | |
| tempoMedioMs: 0, | |
| }) as { | |
| total: number; | |
| sucessos: number; | |
| falhas: number; | |
| tempoMedioMs: number; | |
| }; | |
| const taxaSucesso = resumoProc.total > 0 ? (resumoProc.sucessos / resumoProc.total) * 100 : 0; | |
| const resumoErros = (resumoErrosPeriodo[0] || { | |
| total: 0, | |
| operacionais: 0, | |
| cliente: 0, | |
| adminAuth: 0, | |
| }) as { | |
| total: number; | |
| operacionais: number; | |
| cliente: number; | |
| adminAuth: number; | |
| }; | |
| const taxaErroAcesso = acessosPeriodo > 0 ? (resumoErros.operacionais / acessosPeriodo) * 100 : 0; | |
| const taxaErroCliente = acessosPeriodo > 0 ? (resumoErros.cliente / acessosPeriodo) * 100 : 0; | |
| const ipsUnicos = ipsUnicosPeriodo.length; | |
| const ipsProcessamento = ipsProcessamentoUnicos.length; | |
| const conversaoAcessoParaProcessamento = | |
| acessosPeriodo > 0 ? (resumoProc.total / acessosPeriodo) * 100 : 0; | |
| const conversaoVisitanteParaProcessamento = | |
| ipsUnicos > 0 ? (ipsProcessamento / ipsUnicos) * 100 : 0; | |
| const retidos = retencaoPorIp.filter((x: { diasAtivos: number }) => x.diasAtivos >= 2).length; | |
| const superEngajados = retencaoPorIp.filter( | |
| (x: { diasAtivos: number }) => x.diasAtivos >= 5, | |
| ).length; | |
| const taxaRetencao = ipsUnicos > 0 ? (retidos / ipsUnicos) * 100 : 0; | |
| const usuariosPagantesProxy = processamentosPorIp.filter( | |
| (x: { total: number }) => x.total >= 5, | |
| ).length; | |
| const taxaPagantesProxy = | |
| ipsProcessamento > 0 ? (usuariosPagantesProxy / ipsProcessamento) * 100 : 0; | |
| const correcoesFeedback = Math.max(0, totalFeedbackPeriodo - feedbackAcertos); | |
| const taxaAcertoFeedback = | |
| totalFeedbackPeriodo > 0 ? (feedbackAcertos / totalFeedbackPeriodo) * 100 : 0; | |
| const precisaoPorInstrumento = feedbackPrecisaoPorInstrumento.map( | |
| (item: { _id: string; total: number; acertos: number }) => ({ | |
| instrumento: item._id || 'nao-informado', | |
| total: item.total, | |
| acertos: item.acertos, | |
| taxaAcerto: item.total > 0 ? Number(((item.acertos / item.total) * 100).toFixed(2)) : 0, | |
| }), | |
| ); | |
| const resumoLearning = (learningResumo[0] || { | |
| total: 0, | |
| explicitos: 0, | |
| implicitos: 0, | |
| sistemicos: 0, | |
| processCompleted: 0, | |
| resultViewed: 0, | |
| confirmations: 0, | |
| corrections: 0, | |
| partialCorrections: 0, | |
| quickAbandons: 0, | |
| reprocesses: 0, | |
| instrumentChanges: 0, | |
| transpositions: 0, | |
| stageUses: 0, | |
| liveSessionSummaries: 0, | |
| }) as Record<string, number>; | |
| const resumoLearningLive = (learningResumoLive[0] || { | |
| total: 0, | |
| avgDurationMs: 0, | |
| avgFirstUsefulMs: 0, | |
| avgLatencyMs: 0, | |
| avgFallbacks: 0, | |
| avgAmbiguityMoments: 0, | |
| }) as Record<string, number>; | |
| const resumoTraces = (decisionTraceResumo[0] || { | |
| total: 0, | |
| memoriaAplicada: 0, | |
| comPerfilAdaptativo: 0, | |
| comQuaseVitoria: 0, | |
| }) as Record<string, number>; | |
| const correctionSignals = Number(resumoLearning.corrections || 0) + Number(resumoLearning.partialCorrections || 0); | |
| const correctionRate = Number(resumoLearning.processCompleted || 0) > 0 | |
| ? (correctionSignals / Number(resumoLearning.processCompleted || 0)) * 100 | |
| : 0; | |
| const quickAbandonRate = Number(resumoLearning.resultViewed || 0) > 0 | |
| ? (Number(resumoLearning.quickAbandons || 0) / Number(resumoLearning.resultViewed || 0)) * 100 | |
| : 0; | |
| const reprocessRate = Number(resumoLearning.processCompleted || 0) > 0 | |
| ? (Number(resumoLearning.reprocesses || 0) / Number(resumoLearning.processCompleted || 0)) * 100 | |
| : 0; | |
| const memoryUsageRate = Number(resumoTraces.total || 0) > 0 | |
| ? (Number(resumoTraces.memoriaAplicada || 0) / Number(resumoTraces.total || 0)) * 100 | |
| : 0; | |
| return { | |
| periodo: { | |
| range: periodo.range, | |
| descricao: periodo.descricao, | |
| bucket: periodo.bucket, | |
| inicio: periodo.inicio, | |
| fim: periodo.fim, | |
| inicioIso: periodo.inicio.toISOString(), | |
| fimIso: periodo.fim.toISOString(), | |
| timezone: 'UTC', | |
| }, | |
| acessos: { | |
| totalGeral: totalAcessos, | |
| totalPeriodo: acessosPeriodo, | |
| ipsUnicosPeriodo: ipsUnicos, | |
| }, | |
| processamento: { | |
| total: resumoProc.total, | |
| sucessos: resumoProc.sucessos, | |
| falhas: resumoProc.falhas, | |
| taxaSucesso: Number(taxaSucesso.toFixed(2)), | |
| tempoMedioMs: Number((resumoProc.tempoMedioMs || 0).toFixed(2)), | |
| porInstrumento: processamentosPorInstrumento.map( | |
| (item: { _id: string; total: number; falhas: number }) => ({ | |
| instrumento: item._id || 'nao-informado', | |
| total: item.total, | |
| falhas: item.falhas, | |
| }), | |
| ), | |
| porDificuldade: processamentosPorDificuldade.map((item: { _id: string; total: number }) => ({ | |
| dificuldade: item._id || 'nao-informada', | |
| total: item.total, | |
| })), | |
| porOrigem: processamentosPorOrigem.map( | |
| (item: { _id: { usouAudio?: boolean; usouTexto?: boolean }; total: number }) => ({ | |
| origem: | |
| item._id?.usouAudio && item._id?.usouTexto | |
| ? 'audio+texto' | |
| : item._id?.usouAudio | |
| ? 'audio' | |
| : item._id?.usouTexto | |
| ? 'texto' | |
| : 'nao-definida', | |
| total: item.total, | |
| }), | |
| ), | |
| }, | |
| monetizacao: { | |
| conversaoAcessoParaProcessamento: Number(conversaoAcessoParaProcessamento.toFixed(2)), | |
| conversaoVisitanteParaProcessamento: Number( | |
| conversaoVisitanteParaProcessamento.toFixed(2), | |
| ), | |
| taxaRetencaoProxy: Number(taxaRetencao.toFixed(2)), | |
| visitantesRetidos: retidos, | |
| visitantesSuperEngajados: superEngajados, | |
| taxaPagantesProxy: Number(taxaPagantesProxy.toFixed(2)), | |
| usuariosPagantesProxy, | |
| taxaErroAcesso: Number(taxaErroAcesso.toFixed(2)), | |
| taxaErroClienteAcesso: Number(taxaErroCliente.toFixed(2)), | |
| taxaAcertoFeedback: Number(taxaAcertoFeedback.toFixed(2)), | |
| topUsuariosEngajados: processamentosPorIp.map((item: { _id: string; total: number }) => ({ | |
| ip: item._id || 'sem-ip', | |
| totalProcessamentos: item.total, | |
| })), | |
| }, | |
| produto: { | |
| endpointsMaisAcessados: endpointsMaisAcessados.map( | |
| (item: { _id: string; total: number; erros: number; tempoMedioMs: number }) => ({ | |
| endpoint: item._id, | |
| total: item.total, | |
| erros: item.erros, | |
| tempoMedioMs: Number((item.tempoMedioMs || 0).toFixed(2)), | |
| }), | |
| ), | |
| tendencia: tendencia.map((item: { _id: string; total: number; erros: number }) => ({ | |
| bucket: item._id, | |
| total: item.total, | |
| erros: item.erros, | |
| })), | |
| fontesResultado: fonteResultados.map((item: { _id: string; total: number }) => ({ | |
| fonte: item._id || 'nao-informada', | |
| total: item.total, | |
| })), | |
| topMusicas: topMusicas.map((item: { _id: { titulo: string; artista: string }; total: number }) => ({ | |
| titulo: item._id.titulo, | |
| artista: item._id.artista, | |
| total: item.total, | |
| })), | |
| }, | |
| erros: { | |
| totalPeriodo: totalErrosPeriodo, | |
| operacionaisPeriodo: resumoErros.operacionais, | |
| clientePeriodo: resumoErros.cliente, | |
| adminAuthPeriodo: resumoErros.adminAuth, | |
| porTipo: errosPorTipo.map((item: { _id: string; total: number }) => ({ | |
| tipo: item._id || 'ErroSemNome', | |
| total: item.total, | |
| })), | |
| recentes: errosRecentes, | |
| }, | |
| aprendizado: { | |
| totalFeedbackPeriodo, | |
| acertosFeedback: feedbackAcertos, | |
| correcoesFeedback, | |
| taxaAcertoFeedback: Number(taxaAcertoFeedback.toFixed(2)), | |
| precisaoPorInstrumento, | |
| topCorrecoes: feedbackCorrecoesTop.map( | |
| (item: { _id: { instrumento: string; previsto: string; correto: string }; total: number }) => ({ | |
| instrumento: item._id.instrumento || 'nao-informado', | |
| previsto: item._id.previsto || '-', | |
| correto: item._id.correto || '-', | |
| total: item.total, | |
| }), | |
| ), | |
| learningEvents: { | |
| total: Number(resumoLearning.total || 0), | |
| explicitos: Number(resumoLearning.explicitos || 0), | |
| implicitos: Number(resumoLearning.implicitos || 0), | |
| sistemicos: Number(resumoLearning.sistemicos || 0), | |
| processCompleted: Number(resumoLearning.processCompleted || 0), | |
| resultViewed: Number(resumoLearning.resultViewed || 0), | |
| confirmations: Number(resumoLearning.confirmations || 0), | |
| corrections: Number(resumoLearning.corrections || 0), | |
| partialCorrections: Number(resumoLearning.partialCorrections || 0), | |
| quickAbandons: Number(resumoLearning.quickAbandons || 0), | |
| reprocesses: Number(resumoLearning.reprocesses || 0), | |
| instrumentChanges: Number(resumoLearning.instrumentChanges || 0), | |
| transpositions: Number(resumoLearning.transpositions || 0), | |
| stageUses: Number(resumoLearning.stageUses || 0), | |
| liveSessionSummaries: Number(resumoLearning.liveSessionSummaries || 0), | |
| correctionRate: Number(correctionRate.toFixed(2)), | |
| quickAbandonRate: Number(quickAbandonRate.toFixed(2)), | |
| reprocessRate: Number(reprocessRate.toFixed(2)), | |
| liveMode: { | |
| totalSessoes: Number(resumoLearningLive.total || 0), | |
| latenciaMediaMs: Number((resumoLearningLive.avgLatencyMs || 0).toFixed(2)), | |
| primeiraLeituraMediaMs: Number((resumoLearningLive.avgFirstUsefulMs || 0).toFixed(2)), | |
| duracaoMediaMs: Number((resumoLearningLive.avgDurationMs || 0).toFixed(2)), | |
| fallbacksMedios: Number((resumoLearningLive.avgFallbacks || 0).toFixed(2)), | |
| ambiguidadesMedias: Number((resumoLearningLive.avgAmbiguityMoments || 0).toFixed(2)), | |
| }, | |
| porEvento: learningPorEvento.map((item: { _id: string; total: number }) => ({ | |
| evento: item._id || 'nao-informado', | |
| total: item.total, | |
| })), | |
| porInstrumento: learningPorInstrumento.map( | |
| (item: { | |
| _id: string; | |
| total: number; | |
| corrections: number; | |
| quickAbandons: number; | |
| reprocesses: number; | |
| liveSessionSummaries?: number; | |
| avgFirstUsefulMs?: number; | |
| avgLatencyMs?: number; | |
| avgFallbacks?: number; | |
| }) => ({ | |
| instrumento: item._id || 'nao-informado', | |
| total: item.total, | |
| correcoes: item.corrections, | |
| abandonosRapidos: item.quickAbandons, | |
| reprocessos: item.reprocesses, | |
| sessoesLive: Number(item.liveSessionSummaries || 0), | |
| primeiraLeituraMediaMs: Number((item.avgFirstUsefulMs || 0).toFixed(2)), | |
| latenciaMediaMs: Number((item.avgLatencyMs || 0).toFixed(2)), | |
| fallbacksMedios: Number((item.avgFallbacks || 0).toFixed(2)), | |
| }), | |
| ), | |
| }, | |
| decisao: { | |
| totalTraces: Number(resumoTraces.total || 0), | |
| memoriaAplicada: Number(resumoTraces.memoriaAplicada || 0), | |
| taxaUsoMemoria: Number(memoryUsageRate.toFixed(2)), | |
| comPerfilAdaptativo: Number(resumoTraces.comPerfilAdaptativo || 0), | |
| comQuaseVitoria: Number(resumoTraces.comQuaseVitoria || 0), | |
| porInstrumento: decisionTracePorInstrumento.map( | |
| (item: { _id: string; total: number; memoriaAplicada: number }) => ({ | |
| instrumento: item._id || 'nao-informado', | |
| total: item.total, | |
| memoriaAplicada: item.memoriaAplicada, | |
| }), | |
| ), | |
| }, | |
| priorsAdaptativos: adaptivePriorsRecentes.map((item: Record<string, unknown>) => ({ | |
| scopeType: String(item.scopeType || 'nao-informado'), | |
| scopeKey: String(item.scopeKey || 'nao-informado'), | |
| lastEventAt: item.lastEventAt || null, | |
| weights: item.weights || {}, | |
| counters: item.counters || {}, | |
| })), | |
| }, | |
| }; | |
| } | |
| private resolverPeriodo(filtro: DashboardFiltro): PeriodoResolvido { | |
| const now = new Date(); | |
| const range = (filtro.range || '30d').trim().toLowerCase(); | |
| let janelaHoras = 24 * 30; | |
| let descricao = 'Ultimos 30 dias'; | |
| if (range === '1h') { | |
| janelaHoras = 1; | |
| descricao = 'Ultima 1 hora'; | |
| } else if (range === '24h') { | |
| janelaHoras = 24; | |
| descricao = 'Ultimas 24 horas'; | |
| } else if (range === '7d') { | |
| janelaHoras = 24 * 7; | |
| descricao = 'Ultimos 7 dias'; | |
| } else if (range === '30d') { | |
| janelaHoras = 24 * 30; | |
| descricao = 'Ultimos 30 dias'; | |
| } else if (range === '90d') { | |
| janelaHoras = 24 * 90; | |
| descricao = 'Ultimos 90 dias'; | |
| } else if (range === 'custom') { | |
| const horasCustom = Number.isFinite(filtro.horas) ? Math.max(1, Math.min(24 * 90, Number(filtro.horas))) : 0; | |
| const diasCustom = Number.isFinite(filtro.dias) ? Math.max(1, Math.min(90, Number(filtro.dias))) : 0; | |
| if (horasCustom > 0) { | |
| janelaHoras = horasCustom; | |
| descricao = `Janela customizada: ultimas ${horasCustom} horas`; | |
| } else if (diasCustom > 0) { | |
| janelaHoras = diasCustom * 24; | |
| descricao = `Janela customizada: ultimos ${diasCustom} dias`; | |
| } | |
| } | |
| const inicio = new Date(now.getTime() - janelaHoras * 60 * 60 * 1000); | |
| return { | |
| inicio, | |
| fim: now, | |
| janelaHoras, | |
| range, | |
| descricao, | |
| bucket: janelaHoras <= 48 ? 'hora' : 'dia', | |
| }; | |
| } | |
| } | |