Claude-skill-registry db-guardian

Especialista Sênior em MongoDB, Segurança de Dados, Migrations, Backup/Recovery e Data Integrity. Guardian dos dados do Super Cartola Manager com foco em operações seguras, auditoria de schemas, otimização de queries e gestão de lifecycle de dados. Use para migrations, limpeza, manutenção, snapshots, índices, validações e qualquer operação crítica com banco de dados.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/db-guardian" ~/.claude/skills/majiayu000-claude-skill-registry-db-guardian && rm -rf "$T"
manifest: skills/data/db-guardian/SKILL.md
source content

DB Guardian Skill (MongoDB Master Edition)

🎯 Missão

Proteger a integridade dos dados do Super Cartola Manager através de operações seguras, backup estratégico, migrations controladas e monitoramento proativo.


1. 🛡️ Protocolo de Segurança Máxima (Data Safety)

1.1 Regras Invioláveis

NUNCA fazer sem backup prévio:

  • deleteMany()
    em qualquer collection
  • drop()
    de collections ou database
  • updateMany()
    sem filtro específico
  • replaceOne()
    em documentos críticos
  • Qualquer operação que modifique > 100 documentos

Collections INTOCÁVEIS (nunca deletar/resetar):

const PROTECTED_COLLECTIONS = [
  'users',              // Contas de acesso
  'times',              // Identidade visual/nomes
  'system_config',      // Configurações globais
  'ligas',              // Definições de ligas
  'audit_logs'          // Histórico de auditoria
];

1.2 Checklist Pré-Operação Destrutiva

□ Backup criado e verificado
□ Operação testada em ambiente de dev/staging
□ Filtros validados (liga_id, temporada, etc)
□ Rollback plan documentado
□ Aprovação do tech lead (se produção)
□ Horário de baixa atividade escolhido
□ Monitoramento ativo preparado

1.3 Pattern de Backup Obrigatório

// SEMPRE antes de operações destrutivas
async function backupBeforeOperation(collection, filter, operationName) {
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  const backupPath = `data/backups/${operationName}_${timestamp}.json`;
  
  // 1. Extrair dados
  const data = await db.collection(collection).find(filter).toArray();
  
  // 2. Salvar backup
  fs.writeFileSync(
    backupPath,
    JSON.stringify({
      timestamp: new Date(),
      collection,
      filter,
      operation: operationName,
      count: data.length,
      data
    }, null, 2)
  );
  
  // 3. Verificar integridade
  const backup = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
  if (backup.count !== data.length) {
    throw new Error('Backup integrity check failed');
  }
  
  console.log(`✅ Backup criado: ${backupPath} (${data.length} docs)`);
  return backupPath;
}

// Exemplo de uso
const backupPath = await backupBeforeOperation(
  'rodadas',
  { liga_id: ligaId, temporada: '2025' },
  'cleanup_2025'
);

// Só então executar operação
await db.collection('rodadas').deleteMany({ 
  liga_id: ligaId, 
  temporada: '2025' 
});

2. 🔄 Virada de Temporada (Season Turnover)

2.1 Arquitetura de Dados por Temporada

data/
├── history/
│   ├── 2025/
│   │   ├── metadata.json          # Metadados da temporada
│   │   ├── final_standings.json   # Classificação final
│   │   ├── financial_summary.json # Resumo financeiro
│   │   ├── champions.json         # Campeões de cada disputa
│   │   └── participants.json      # Lista de participantes
│   └── 2026/
│       └── ... (mesmo padrão)
├── backups/
│   └── YYYY-MM-DD_HH-MM-SS/
│       └── ... (backups automáticos)
└── users_registry.json            # Cartório vitalício

2.2 Script de Virada de Temporada (Turn Key)

Criar

/scripts/turn_key_2026.js
:

const mongoose = require('mongoose');
const fs = require('fs');
const path = require('path');

// ==================== CONFIGURAÇÃO ====================
const CURRENT_SEASON = '2025';
const NEW_SEASON = '2026';
const TURNOVER_DATE = new Date('2026-01-01T00:00:00Z');

// Trava de segurança por data
if (Date.now() < TURNOVER_DATE.getTime()) {
  console.error('🔴 ERRO: Virada de temporada só pode ser executada após 01/01/2026');
  console.error(`Data atual: ${new Date().toISOString()}`);
  console.error(`Data permitida: ${TURNOVER_DATE.toISOString()}`);
  process.exit(1);
}

// ==================== CONEXÃO ====================
const MONGO_URI = process.env.MONGO_URI;

async function connectDB() {
  try {
    await mongoose.connect(MONGO_URI);
    console.log('✅ Conectado ao MongoDB');
  } catch (error) {
    console.error('🔴 Erro ao conectar:', error);
    process.exit(1);
  }
}

// ==================== SNAPSHOT FINAL ====================
async function createSeasonSnapshot(season) {
  const snapshotPath = `data/history/${season}`;
  
  // Criar diretório se não existir
  if (!fs.existsSync(snapshotPath)) {
    fs.mkdirSync(snapshotPath, { recursive: true });
  }
  
  console.log(`📸 Criando snapshot da temporada ${season}...`);
  
  // 1. Metadados
  const metadata = {
    season,
    snapshotDate: new Date(),
    totalParticipants: await mongoose.model('Participante').countDocuments({ 
      temporada: season 
    }),
    totalRodadas: await mongoose.model('Rodada').countDocuments({ 
      temporada: season 
    })
  };
  
  fs.writeFileSync(
    path.join(snapshotPath, 'metadata.json'),
    JSON.stringify(metadata, null, 2)
  );
  
  // 2. Classificação Final (todas ligas)
  const ligas = await mongoose.model('Liga').find({}).lean();
  const finalStandings = {};
  
  for (const liga of ligas) {
    const ranking = await mongoose.model('Participante')
      .find({ liga_id: liga._id, temporada: season })
      .select('nome pontos_acumulados posicao_final')
      .sort({ pontos_acumulados: -1 })
      .lean();
    
    finalStandings[liga.nome] = ranking;
  }
  
  fs.writeFileSync(
    path.join(snapshotPath, 'final_standings.json'),
    JSON.stringify(finalStandings, null, 2)
  );
  
  // 3. Resumo Financeiro
  const financialSummary = await createFinancialSummary(season);
  fs.writeFileSync(
    path.join(snapshotPath, 'financial_summary.json'),
    JSON.stringify(financialSummary, null, 2)
  );
  
  // 4. Campeões
  const champions = await extractChampions(season);
  fs.writeFileSync(
    path.join(snapshotPath, 'champions.json'),
    JSON.stringify(champions, null, 2)
  );
  
  console.log(`✅ Snapshot completo salvo em ${snapshotPath}`);
}

async function createFinancialSummary(season) {
  const summary = {
    season,
    totalCredito: 0,
    totalDebito: 0,
    saldoGeral: 0,
    porLiga: {}
  };
  
  const ligas = await mongoose.model('Liga').find({}).lean();
  
  for (const liga of ligas) {
    const participantes = await mongoose.model('Participante')
      .find({ liga_id: liga._id, temporada: season })
      .lean();
    
    let credito = 0;
    let debito = 0;
    
    participantes.forEach(p => {
      const saldo = p.saldo_temporada || 0;
      if (saldo > 0) credito += saldo;
      else debito += Math.abs(saldo);
    });
    
    summary.porLiga[liga.nome] = {
      credito,
      debito,
      saldo: credito - debito,
      participantes: participantes.length
    };
    
    summary.totalCredito += credito;
    summary.totalDebito += debito;
  }
  
  summary.saldoGeral = summary.totalCredito - summary.totalDebito;
  
  return summary;
}

async function extractChampions(season) {
  const champions = {
    pontosCorridos: {},
    mataMata: {},
    artilheiro: {},
    luvaDeOuro: {},
    melhorDoMes: {}
  };
  
  const ligas = await mongoose.model('Liga').find({}).lean();
  
  for (const liga of ligas) {
    // Pontos Corridos - 1º lugar
    const pcWinner = await mongoose.model('PontosCorridos')
      .findOne({ liga_id: liga._id, temporada: season })
      .sort({ pontos: -1 })
      .populate('participante_id', 'nome')
      .lean();
    
    if (pcWinner) {
      champions.pontosCorridos[liga.nome] = {
        participante: pcWinner.participante_id?.nome,
        pontos: pcWinner.pontos
      };
    }
    
    // Mata-Mata - Campeão
    const mmChampion = await mongoose.model('MataMata')
      .findOne({ 
        liga_id: liga._id, 
        temporada: season,
        fase: 'final',
        vencedor: true
      })
      .populate('participante_id', 'nome')
      .lean();
    
    if (mmChampion) {
      champions.mataMata[liga.nome] = {
        participante: mmChampion.participante_id?.nome
      };
    }
    
    // Artilheiro - Maior pontuação única
    const artilheiro = await mongoose.model('Rodada')
      .findOne({ liga_id: liga._id, temporada: season })
      .sort({ pontos_rodada: -1 })
      .populate('participante_id', 'nome')
      .lean();
    
    if (artilheiro) {
      champions.artilheiro[liga.nome] = {
        participante: artilheiro.participante_id?.nome,
        pontos: artilheiro.pontos_rodada,
        rodada: artilheiro.rodada_num
      };
    }
  }
  
  return champions;
}

// ==================== LIMPEZA SELETIVA ====================
async function cleanupSeasonData(season) {
  console.log(`🧹 Limpando dados da temporada ${season}...`);
  
  // Collections a limpar (dados de jogo)
  const collectionsToClean = [
    'rodadas',
    'rankings',
    'pontos_corridos',
    'mata_mata',
    'top10',
    'financeiro_cache'
  ];
  
  for (const collName of collectionsToClean) {
    try {
      const result = await mongoose.connection.db
        .collection(collName)
        .deleteMany({ temporada: season });
      
      console.log(`  ✅ ${collName}: ${result.deletedCount} docs removidos`);
    } catch (error) {
      console.error(`  🔴 Erro ao limpar ${collName}:`, error.message);
    }
  }
  
  console.log('✅ Limpeza concluída');
}

// ==================== PRESERVAÇÃO ====================
async function updateUsersRegistry() {
  console.log('📝 Atualizando users_registry.json...');
  
  const users = await mongoose.model('User').find({}).lean();
  
  const registry = users.map(user => ({
    _id: user._id,
    email: user.email,
    nome: user.nome,
    active_seasons: user.active_seasons,
    created_at: user.created_at,
    last_updated: new Date()
  }));
  
  fs.writeFileSync(
    'data/users_registry.json',
    JSON.stringify(registry, null, 2)
  );
  
  console.log(`✅ Registry atualizado (${users.length} usuários)`);
}

// ==================== PREPARAÇÃO NOVA TEMPORADA ====================
async function prepareNewSeason(season) {
  console.log(`🚀 Preparando temporada ${season}...`);
  
  // 1. Criar configuração da nova temporada
  const config = {
    temporada: season,
    rodada_atual: 1,
    ativa: true,
    data_inicio: new Date(`${season}-03-01`),
    data_fim: new Date(`${season}-12-31`)
  };
  
  await mongoose.connection.db
    .collection('system_config')
    .updateOne(
      { tipo: 'temporada_atual' },
      { $set: config },
      { upsert: true }
    );
  
  // 2. Resetar saldos dos participantes
  await mongoose.model('Participante').updateMany(
    {},
    { 
      $set: { 
        saldo_temporada: 0,
        pontos_acumulados: 0
      }
    }
  );
  
  console.log(`✅ Temporada ${season} preparada`);
}

// ==================== VALIDAÇÕES ====================
async function validateTurnover() {
  console.log('🔍 Executando validações...');
  
  const validations = [];
  
  // 1. Verificar se temporada atual ainda tem dados
  const currentData = await mongoose.model('Rodada')
    .countDocuments({ temporada: CURRENT_SEASON });
  
  if (currentData === 0) {
    validations.push('⚠️  Nenhuma rodada encontrada para temporada atual');
  } else {
    validations.push(`✅ ${currentData} rodadas na temporada ${CURRENT_SEASON}`);
  }
  
  // 2. Verificar se já existe snapshot
  const snapshotPath = `data/history/${CURRENT_SEASON}`;
  if (fs.existsSync(snapshotPath)) {
    validations.push('⚠️  Snapshot já existe - será sobrescrito');
  }
  
  // 3. Verificar espaço em disco
  const diskUsage = await checkDiskSpace();
  if (diskUsage > 90) {
    validations.push(`🔴 Espaço em disco crítico: ${diskUsage}%`);
  } else {
    validations.push(`✅ Espaço em disco OK: ${diskUsage}%`);
  }
  
  validations.forEach(v => console.log(`  ${v}`));
  
  return validations;
}

function checkDiskSpace() {
  // Implementação simplificada
  return 50; // Placeholder
}

// ==================== EXECUÇÃO PRINCIPAL ====================
async function main() {
  console.log('╔════════════════════════════════════════════╗');
  console.log('║   VIRADA DE TEMPORADA - SUPER CARTOLA      ║');
  console.log(`║   ${CURRENT_SEASON} → ${NEW_SEASON}                          ║`);
  console.log('╚════════════════════════════════════════════╝');
  console.log('');
  
  const isDryRun = process.argv.includes('--dry-run');
  
  if (isDryRun) {
    console.log('🔵 MODO DRY-RUN (simulação)');
    console.log('');
  }
  
  try {
    await connectDB();
    
    // 1. Validações
    await validateTurnover();
    console.log('');
    
    // 2. Confirmação (se não for dry-run)
    if (!isDryRun) {
      console.log('⚠️  ATENÇÃO: Esta operação irá:');
      console.log('  - Criar snapshot da temporada atual');
      console.log('  - Limpar dados de jogo (rodadas, rankings, etc)');
      console.log('  - Preparar nova temporada');
      console.log('');
      console.log('Para confirmar, adicione --confirm ao comando');
      
      if (!process.argv.includes('--confirm')) {
        console.log('');
        console.log('Comando cancelado. Use:');
        console.log('  node scripts/turn_key_2026.js --confirm');
        process.exit(0);
      }
    }
    
    // 3. Snapshot final
    if (!isDryRun) {
      await createSeasonSnapshot(CURRENT_SEASON);
    } else {
      console.log('📸 [DRY-RUN] Criaria snapshot de', CURRENT_SEASON);
    }
    
    // 4. Atualizar registry
    if (!isDryRun) {
      await updateUsersRegistry();
    } else {
      console.log('📝 [DRY-RUN] Atualizaria users_registry.json');
    }
    
    // 5. Limpeza
    if (!isDryRun) {
      await cleanupSeasonData(CURRENT_SEASON);
    } else {
      console.log('🧹 [DRY-RUN] Limparia dados de', CURRENT_SEASON);
    }
    
    // 6. Preparar nova temporada
    if (!isDryRun) {
      await prepareNewSeason(NEW_SEASON);
    } else {
      console.log('🚀 [DRY-RUN] Prepararia temporada', NEW_SEASON);
    }
    
    console.log('');
    console.log('═══════════════════════════════════════════');
    console.log('✅ VIRADA DE TEMPORADA CONCLUÍDA COM SUCESSO');
    console.log('═══════════════════════════════════════════');
    
  } catch (error) {
    console.error('');
    console.error('🔴 ERRO DURANTE VIRADA DE TEMPORADA:', error);
    console.error('');
    console.error('Stack:', error.stack);
    process.exit(1);
  } finally {
    await mongoose.disconnect();
    console.log('');
    console.log('Desconectado do MongoDB');
  }
}

// Executar
if (require.main === module) {
  main();
}

module.exports = { createSeasonSnapshot, cleanupSeasonData };

Uso:

# Testar (dry-run)
node scripts/turn_key_2026.js --dry-run

# Executar de verdade (após 01/01/2026)
node scripts/turn_key_2026.js --confirm

3. 👥 Gestão de Acesso (User Management)

3.1 Schema de Active Seasons

// Model: User
const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  nome: String,
  active_seasons: { 
    type: [String], 
    default: [] 
  },  // Ex: ["2025", "2026"]
  created_at: { type: Date, default: Date.now },
  last_login: Date
});

// Middleware de autenticação
function checkSeasonAccess(req, res, next) {
  const { temporada } = req.params;
  const user = req.session.user;
  
  if (!user.active_seasons.includes(temporada)) {
    return res.status(403).json({
      error: 'Acesso negado',
      message: 'Usuário não tem acesso a esta temporada',
      active_seasons: user.active_seasons
    });
  }
  
  next();
}

3.2 Script de Renovação de Usuários

Criar

/scripts/admin_renew_user.js
:

const mongoose = require('mongoose');
const User = require('../models/User');

async function listPendingRenewals(season) {
  const users = await User.find({
    active_seasons: { $ne: season }
  }).select('email nome active_seasons');
  
  console.log(`📋 Usuários pendentes de renovação para ${season}:`);
  console.log('');
  
  users.forEach((user, index) => {
    console.log(`${index + 1}. ${user.email} (${user.nome})`);
    console.log(`   Temporadas ativas: ${user.active_seasons.join(', ')}`);
  });
  
  console.log('');
  console.log(`Total: ${users.length} usuários`);
}

async function renewUser(userId, season) {
  const user = await User.findById(userId);
  
  if (!user) {
    throw new Error('Usuário não encontrado');
  }
  
  if (user.active_seasons.includes(season)) {
    console.log(`⚠️  Usuário ${user.email} já tem acesso a ${season}`);
    return;
  }
  
  user.active_seasons.push(season);
  await user.save();
  
  console.log(`✅ Renovado: ${user.email} → temporadas: ${user.active_seasons.join(', ')}`);
  
  // Audit log
  await createAuditLog({
    action: 'USER_RENEWAL',
    target: user._id,
    details: { season, by: 'admin_script' }
  });
}

async function revokeAccess(userId, season) {
  const user = await User.findById(userId);
  
  if (!user) {
    throw new Error('Usuário não encontrado');
  }
  
  user.active_seasons = user.active_seasons.filter(s => s !== season);
  await user.save();
  
  console.log(`🔴 Revogado: ${user.email} → temporadas: ${user.active_seasons.join(', ')}`);
  
  // Audit log
  await createAuditLog({
    action: 'USER_REVOCATION',
    target: user._id,
    details: { season, by: 'admin_script' }
  });
}

async function getStats(season) {
  const total = await User.countDocuments();
  const active = await User.countDocuments({ active_seasons: season });
  const pending = total - active;
  
  console.log('📊 Estatísticas de Renovação');
  console.log('');
  console.log(`Temporada: ${season}`);
  console.log(`Total de usuários: ${total}`);
  console.log(`Com acesso: ${active} (${((active/total)*100).toFixed(1)}%)`);
  console.log(`Pendentes: ${pending} (${((pending/total)*100).toFixed(1)}%)`);
}

// CLI
const args = process.argv.slice(2);
const command = args[0];
const season = process.env.CURRENT_SEASON || '2026';

async function main() {
  await mongoose.connect(process.env.MONGO_URI);
  
  try {
    switch(command) {
      case '--list-pending':
        await listPendingRenewals(season);
        break;
      
      case '--user':
        const userId = args[1];
        if (!userId) {
          console.error('Erro: userId não fornecido');
          process.exit(1);
        }
        
        if (args.includes('--revoke')) {
          await revokeAccess(userId, season);
        } else {
          await renewUser(userId, season);
        }
        break;
      
      case '--stats':
        await getStats(season);
        break;
      
      default:
        console.log('Uso:');
        console.log('  node scripts/admin_renew_user.js --list-pending');
        console.log('  node scripts/admin_renew_user.js --user <userId>');
        console.log('  node scripts/admin_renew_user.js --user <userId> --revoke');
        console.log('  node scripts/admin_renew_user.js --stats');
    }
  } finally {
    await mongoose.disconnect();
  }
}

main();

4. 📊 Monitoramento e Diagnóstico

4.1 Script de Health Check do DB

Criar

/scripts/db_health_check.js
:

async function checkDBHealth() {
  const health = {
    timestamp: new Date(),
    status: 'unknown',
    checks: {}
  };
  
  try {
    // 1. Conexão
    const startTime = Date.now();
    await mongoose.connection.db.admin().ping();
    health.checks.connection = {
      status: 'ok',
      latency: Date.now() - startTime
    };
    
    // 2. Collections
    const collections = await mongoose.connection.db.listCollections().toArray();
    health.checks.collections = {
      status: 'ok',
      count: collections.length,
      names: collections.map(c => c.name)
    };
    
    // 3. Índices
    const indexStats = await checkIndexes();
    health.checks.indexes = indexStats;
    
    // 4. Tamanho do DB
    const stats = await mongoose.connection.db.stats();
    health.checks.size = {
      status: 'ok',
      dataSize: (stats.dataSize / 1024 / 1024).toFixed(2) + ' MB',
      storageSize: (stats.storageSize / 1024 / 1024).toFixed(2) + ' MB',
      indexes: (stats.indexSize / 1024 / 1024).toFixed(2) + ' MB'
    };
    
    // 5. Queries lentas
    const slowQueries = await checkSlowQueries();
    health.checks.performance = slowQueries;
    
    // Status geral
    const hasErrors = Object.values(health.checks).some(c => c.status === 'error');
    health.status = hasErrors ? 'degraded' : 'healthy';
    
  } catch (error) {
    health.status = 'error';
    health.error = error.message;
  }
  
  return health;
}

async function checkIndexes() {
  const collections = ['participantes', 'rodadas', 'acertos_financeiros'];
  const indexReport = {};
  
  for (const collName of collections) {
    const indexes = await mongoose.connection.db
      .collection(collName)
      .indexes();
    
    indexReport[collName] = {
      count: indexes.length,
      hasLigaId: indexes.some(idx => idx.key.liga_id),
      details: indexes.map(idx => ({
        name: idx.name,
        keys: Object.keys(idx.key)
      }))
    };
  }
  
  return {
    status: 'ok',
    collections: indexReport
  };
}

async function checkSlowQueries() {
  // Verificar system.profile se ativado
  const profiling = await mongoose.connection.db.command({ profile: -1 });
  
  return {
    status: profiling.was === 0 ? 'disabled' : 'enabled',
    level: profiling.was
  };
}

4.2 Métricas Críticas

// Monitorar estas métricas continuamente
const CRITICAL_METRICS = {
  // Performance
  avgQueryTime: { threshold: 100, unit: 'ms' },          // <100ms
  connectionPoolSize: { threshold: 50, unit: 'connections' },
  
  // Data Integrity
  orphanedDocuments: { threshold: 0, unit: 'docs' },     // 0
  duplicateIds: { threshold: 0, unit: 'docs' },          // 0
  
  // Multi-tenant
  queriesWithoutLigaId: { threshold: 5, unit: 'queries/hour' },
  
  // Storage
  dataGrowthRate: { threshold: 10, unit: '%/month' },
  indexFragmentation: { threshold: 30, unit: '%' }
};

5. 🔧 Migrations e Alterações de Schema

5.1 Pattern de Migration Segura

// /migrations/YYYY-MM-DD_description.js
const mongoose = require('mongoose');

async function up() {
  console.log('🔼 Executando migration: description');
  
  // 1. Backup antes de qualquer coisa
  const backup = await backupCollection('collection_name');
  
  try {
    // 2. Validar estado atual
    const count = await validatePreConditions();
    console.log(`Documentos a migrar: ${count}`);
    
    // 3. Executar em batches (não travar o DB)
    const batchSize = 100;
    let processed = 0;
    
    while (processed < count) {
      const batch = await mongoose.connection.db
        .collection('collection_name')
        .find({})
        .skip(processed)
        .limit(batchSize)
        .toArray();
      
      for (const doc of batch) {
        // Transformação
        await transformDocument(doc);
      }
      
      processed += batchSize;
      console.log(`Progresso: ${processed}/${count}`);
    }
    
    // 4. Validar resultado
    await validatePostConditions();
    
    console.log('✅ Migration concluída');
  } catch (error) {
    console.error('🔴 Migration falhou:', error);
    console.error('Restaurando backup...');
    await restoreFromBackup(backup);
    throw error;
  }
}

async function down() {
  console.log('🔽 Revertendo migration: description');
  // Implementar rollback
}

module.exports = { up, down };

5.2 Migrations Comuns - Super Cartola

Adicionar campo liga_id a collection existente

async function addLigaIdToCollection(collectionName, defaultLigaId) {
  const result = await mongoose.connection.db
    .collection(collectionName)
    .updateMany(
      { liga_id: { $exists: false } },
      { $set: { liga_id: defaultLigaId } }
    );
  
  console.log(`Adicionado liga_id a ${result.modifiedCount} documentos`);
  
  // Criar índice
  await mongoose.connection.db
    .collection(collectionName)
    .createIndex({ liga_id: 1 });
}

Normalizar tipo de campo

async function normalizeFieldType(collectionName, fieldName, transformer) {
  const docs = await mongoose.connection.db
    .collection(collectionName)
    .find({ [fieldName]: { $exists: true } })
    .toArray();
  
  for (const doc of docs) {
    const newValue = transformer(doc[fieldName]);
    
    await mongoose.connection.db
      .collection(collectionName)
      .updateOne(
        { _id: doc._id },
        { $set: { [fieldName]: newValue } }
      );
  }
}

// Exemplo: String → ObjectId
await normalizeFieldType(
  'rodadas',
  'liga_id',
  (value) => mongoose.Types.ObjectId(value)
);

6. 🗂️ Índices e Otimização

6.1 Índices Obrigatórios - Super Cartola

// Executar em cada collection
const REQUIRED_INDEXES = {
  participantes: [
    { liga_id: 1, temporada: 1 },
    { liga_id: 1, email: 1 },
    { user_id: 1 }
  ],
  
  rodadas: [
    { liga_id: 1, temporada: 1, rodada_num: 1 },
    { participante_id: 1, temporada: 1 },
    { liga_id: 1, temporada: 1, pontos_rodada: -1 }  // Para ranking
  ],
  
  acertos_financeiros: [
    { liga_id: 1, temporada: 1 },
    { participante_id: 1, temporada: 1 },
    { idempotency_key: 1 }  // Unique
  ],
  
  pontos_corridos: [
    { liga_id: 1, temporada: 1, pontos: -1 }
  ]
};

async function ensureIndexes() {
  for (const [collName, indexes] of Object.entries(REQUIRED_INDEXES)) {
    console.log(`Verificando índices em ${collName}...`);
    
    for (const index of indexes) {
      const indexName = Object.keys(index).join('_');
      
      try {
        await mongoose.connection.db
          .collection(collName)
          .createIndex(index, { name: indexName, background: true });
        
        console.log(`  ✅ ${indexName}`);
      } catch (error) {
        if (error.code === 85) {
          console.log(`  ⚠️  ${indexName} já existe com definição diferente`);
        } else {
          console.error(`  🔴 Erro ao criar ${indexName}:`, error.message);
        }
      }
    }
  }
}

6.2 Análise de Queries

// Habilitar profiling temporariamente
async function analyzeQueries(durationMinutes = 10) {
  // 1. Ativar profiling
  await mongoose.connection.db.setProfilingLevel(1, { slowms: 100 });
  console.log(`Profiling ativado por ${durationMinutes} minutos`);
  
  // 2. Aguardar
  await new Promise(resolve => setTimeout(resolve, durationMinutes * 60 * 1000));
  
  // 3. Analisar
  const slowQueries = await mongoose.connection.db
    .collection('system.profile')
    .find({ millis: { $gt: 100 } })
    .sort({ millis: -1 })
    .limit(20)
    .toArray();
  
  console.log('Top 20 queries mais lentas:');
  slowQueries.forEach(q => {
    console.log(`  ${q.millis}ms - ${q.ns} - ${JSON.stringify(q.command)}`);
  });
  
  // 4. Desativar profiling
  await mongoose.connection.db.setProfilingLevel(0);
}

7. 📋 Comandos Úteis

7.1 Quick Reference

# === VIRADA DE TEMPORADA ===
node scripts/turn_key_2026.js --dry-run         # Testar
node scripts/turn_key_2026.js --confirm         # Executar

# === GESTÃO DE USUÁRIOS ===
node scripts/admin_renew_user.js --list-pending
node scripts/admin_renew_user.js --user <id>
node scripts/admin_renew_user.js --stats

# === HEALTH CHECK ===
node scripts/db_health_check.js

# === BACKUP MANUAL ===
mongodump --uri="$MONGO_URI" --out=backups/$(date +%Y%m%d)

# === RESTORE ===
mongorestore --uri="$MONGO_URI" backups/YYYYMMDD

# === ANÁLISE ===
mongo --eval "db.stats()"                       # Estatísticas do DB
mongo --eval "db.participantes.getIndexes()"    # Ver índices
mongo --eval "db.participantes.stats()"         # Stats de collection

7.2 MongoDB Shell Snippets

// Contar documentos por temporada
db.rodadas.aggregate([
  { $group: { _id: "$temporada", count: { $sum: 1 } } },
  { $sort: { _id: 1 } }
]);

// Encontrar documentos órfãos (sem liga_id)
db.participantes.find({ liga_id: { $exists: false } }).count();

// Verificar duplicatas
db.participantes.aggregate([
  { $group: { 
      _id: { liga_id: "$liga_id", email: "$email" }, 
      count: { $sum: 1 } 
  }},
  { $match: { count: { $gt: 1 } } }
]);

// Top 10 maiores collections
db.getCollectionNames().map(c => ({ 
  name: c, 
  size: db[c].stats().size 
})).sort((a,b) => b.size - a.size).slice(0, 10);

8. 🚨 Troubleshooting

8.1 Problemas Comuns

ProblemaCausa ProvávelSolução
Queries lentasSem índice
db.collection.createIndex()
Documentos órfãosMigration incompletaRodar script de cleanup
Espaço em discoDados antigos acumuladosVirada de temporada
Conexões esgotadasPool pequenoAumentar connectionPoolSize
Lock timeoutOperação muito grandeDividir em batches

8.2 Recovery Procedures

// Recuperar de backup específico
async function recoverFromBackup(backupPath) {
  console.log(`Recuperando de ${backupPath}...`);
  
  const backup = JSON.parse(fs.readFileSync(backupPath, 'utf8'));
  
  // Validar backup
  if (!backup.collection || !backup.data) {
    throw new Error('Backup inválido');
  }
  
  // Limpar collection atual (com confirmação!)
  console.log(`⚠️  Isso irá DELETAR todos os dados de ${backup.collection}`);
  console.log('Digite "CONFIRMAR" para continuar:');
  
  // ... aguardar confirmação
  
  // Restaurar
  await mongoose.connection.db
    .collection(backup.collection)
    .deleteMany({});
  
  await mongoose.connection.db
    .collection(backup.collection)
    .insertMany(backup.data);
  
  console.log(`✅ Recuperado ${backup.count} documentos`);
}

9. 📚 Best Practices

9.1 Naming Conventions

// Collections: plural, snake_case
'participantes', 'acertos_financeiros', 'rodadas'

// Campos: snake_case
'liga_id', 'participante_id', 'rodada_num'

// Índices: descritivo
'liga_temporada_idx', 'participante_email_unique'

// Backups: timestamp + descrição
'2026-01-15_pre-migration-participantes.json'

9.2 Data Integrity Checks

// Executar periodicamente
async function runIntegrityChecks() {
  const checks = [];
  
  // 1. Órfãos (participantes sem liga)
  const orphans = await mongoose.model('Participante').countDocuments({
    liga_id: { $exists: false }
  });
  checks.push({ name: 'Órfãos', count: orphans, critical: orphans > 0 });
  
  // 2. Duplicatas
  const duplicates = await findDuplicates('participantes', ['liga_id', 'email']);
  checks.push({ name: 'Duplicatas', count: duplicates.length, critical: duplicates.length > 0 });
  
  // 3. Referências quebradas
  const brokenRefs = await checkReferences();
  checks.push({ name: 'Refs quebradas', count: brokenRefs, critical: brokenRefs > 0 });
  
  // Report
  console.log('🔍 Integrity Checks:');
  checks.forEach(c => {
    const icon = c.critical ? '🔴' : '✅';
    console.log(`  ${icon} ${c.name}: ${c.count}`);
  });
  
  return checks;
}

STATUS: 🛡️ DB Guardian - ATIVO & VIGILANTE

Versão: 2.0 (Super Cartola Master Edition)

Última atualização: 2026-01-17