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; } interface RegistrarErroInput { method: string; path: string; statusCode: number; message?: string; errorName?: string; stack?: string; ip?: string; userAgent?: string; requestContext?: Record; } interface DashboardFiltro { range?: string; dias?: number; horas?: number; } interface PeriodoResolvido { inicio: Date; fim: Date; janelaHoras: number; range: string; descricao: string; bucket: 'hora' | 'dia'; } @Injectable() export class AdminService { constructor( @InjectModel(AccessLog.name) private readonly accessLogModel: Model, @InjectModel(ErrorLog.name) private readonly errorLogModel: Model, @InjectModel(Musica.name) private readonly musicaModel: Model, @InjectModel(FeedbackAprendizado.name) private readonly feedbackModel: Model, @InjectModel(LearningEvent.name) private readonly learningEventModel: Model, @InjectModel(DecisionTrace.name) private readonly decisionTraceModel: Model, @InjectModel(AdaptivePrior.name) private readonly adaptivePriorModel: Model, ) {} validarChaveDashboard(chaveInformada: string | undefined): boolean { const chaveEsperada = process.env.DASHBOARD_KEY || 'desenrolaai-dashboard-2026'; return !!chaveInformada && chaveInformada.trim() === chaveEsperada; } async registrarAcesso(input: RegistrarAcessoInput): Promise { await this.accessLogModel.create(input); } async registrarErro(input: RegistrarErroInput): Promise { await this.errorLogModel.create(input); } async obterDashboard(filtro: DashboardFiltro): Promise> { 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; const resumoLearningLive = (learningResumoLive[0] || { total: 0, avgDurationMs: 0, avgFirstUsefulMs: 0, avgLatencyMs: 0, avgFallbacks: 0, avgAmbiguityMoments: 0, }) as Record; const resumoTraces = (decisionTraceResumo[0] || { total: 0, memoriaAplicada: 0, comPerfilAdaptativo: 0, comQuaseVitoria: 0, }) as Record; 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) => ({ 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', }; } }