DesenrolaAi / backend /src /admin /admin.service.ts
Azure DevOps Pipeline
deploy: Merged PR 18: - modo ao vivo
adb1607
Raw
History Blame Contribute Delete
30.3 kB
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';
}
@Injectable()
export class AdminService {
constructor(
@InjectModel(AccessLog.name)
private readonly accessLogModel: Model<AccessLog>,
@InjectModel(ErrorLog.name)
private readonly errorLogModel: Model<ErrorLog>,
@InjectModel(Musica.name)
private readonly musicaModel: Model<Musica>,
@InjectModel(FeedbackAprendizado.name)
private readonly feedbackModel: Model<FeedbackAprendizado>,
@InjectModel(LearningEvent.name)
private readonly learningEventModel: Model<LearningEvent>,
@InjectModel(DecisionTrace.name)
private readonly decisionTraceModel: Model<DecisionTrace>,
@InjectModel(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',
};
}
}