#!/usr/bin/env node // ============================================================================= // AgenciaPsi — Dashboard Generator // ============================================================================= // Uso: // node generate-dashboard.js → usa backup mais recente // node generate-dashboard.js 2026-03-27 → usa backup de data específica // // Lê de: ./database-novo/backups/YYYY-MM-DD/schema.sql // Gera: ./dashboard.html (na mesma pasta do script) // ============================================================================= const fs = require('fs'); const path = require('path'); const BACKUPS_DIR = path.join(__dirname, 'backups'); const OUTPUT_FILE = path.join(__dirname, 'dashboard.html'); // --------------------------------------------------------------------------- // Cores por domínio // --------------------------------------------------------------------------- const DOMAIN_COLORS = { 'SaaS / Planos': '#4f8cff', 'Tenants / Multi-tenant': '#6ee7b7', 'Pacientes': '#f472b6', 'Agenda': '#fb923c', 'Financeiro': '#a78bfa', 'Serviços / Commitments': '#34d399', 'Notificações': '#38bdf8', 'Email / Comunicação': '#fbbf24', 'SaaS Admin': '#94a3b8', }; // --------------------------------------------------------------------------- // Mapeamento domínio → tabelas // Adicione novas tabelas aqui quando criar migrations // --------------------------------------------------------------------------- const DOMAIN_TABLES = { 'SaaS / Planos': [ 'plans','plan_features','plan_prices','plan_public','plan_public_bullets', 'features','modules','module_features','subscriptions','subscription_events', 'subscription_intents_personal','subscription_intents_tenant','subscription_intents_legacy', 'addon_products','addon_credits','addon_transactions', 'tenant_features','tenant_modules','entitlements_invalidation','tenant_feature_exceptions_log', ], 'Tenants / Multi-tenant': [ 'tenants','tenant_members','tenant_invites','owner_users', 'profiles','company_profiles','billing_contracts','payment_settings','support_sessions', ], 'Pacientes': [ 'patients','patient_groups','patient_group_patient','patient_tags', 'patient_patient_tag','patient_discounts','patient_invites','patient_intake_requests', ], 'Agenda': [ 'agenda_eventos','agenda_configuracoes','agenda_bloqueios','agenda_excecoes', 'agenda_online_slots','agenda_regras_semanais','agenda_slots_bloqueados_semanais', 'agenda_slots_regras','agendador_configuracoes','agendador_solicitacoes', 'feriados','recurrence_rules','recurrence_exceptions','recurrence_rule_services', ], 'Financeiro': [ 'financial_records','financial_categories','financial_exceptions', 'therapist_payouts','therapist_payout_records', 'insurance_plans','insurance_plan_services','professional_pricing', ], 'Serviços / Commitments': [ 'services','determined_commitments','determined_commitment_fields', 'commitment_services','commitment_time_logs', ], 'Notificações': [ 'notifications','notification_channels','notification_templates', 'notification_queue','notification_logs','notification_preferences', 'notification_schedules','twilio_subaccount_usage', ], 'Email / Comunicação': [ 'email_templates_global','email_templates_tenant','email_layout_config', 'global_notices','notice_dismissals','login_carousel_slides', ], 'SaaS Admin': [ 'saas_admins','saas_docs','saas_doc_votos','saas_faq','saas_faq_itens', 'user_settings','dev_user_credentials','_db_migrations', ], }; // --------------------------------------------------------------------------- // 1. Resolve qual schema.sql usar // --------------------------------------------------------------------------- function resolveSchema() { const arg = process.argv[2]; if (!fs.existsSync(BACKUPS_DIR)) { console.error(`✖ Pasta não encontrada: ${BACKUPS_DIR}`); console.error(` Certifique-se que o script está na raiz do projeto.`); process.exit(1); } const available = fs.readdirSync(BACKUPS_DIR) .filter(f => /^\d{4}-\d{2}-\d{2}$/.test(f)) .sort() .reverse(); if (available.length === 0) { console.error('✖ Nenhum backup encontrado em database-novo/backups/'); console.error(' Rode primeiro: node db.cjs backup'); process.exit(1); } const date = (arg && /^\d{4}-\d{2}-\d{2}$/.test(arg)) ? arg : available[0]; if (!available.includes(date)) { console.error(`✖ Backup não encontrado para: ${date}`); console.error(` Disponíveis: ${available.join(', ')}`); process.exit(1); } const schemaPath = path.join(BACKUPS_DIR, date, 'schema.sql'); if (!fs.existsSync(schemaPath)) { console.error(`✖ schema.sql não encontrado em database-novo/backups/${date}/`); process.exit(1); } return { schemaPath, date, available }; } // --------------------------------------------------------------------------- // 2. Parse do schema.sql — extrai tabelas, colunas e FKs // --------------------------------------------------------------------------- function parseSchema(content) { const tables = {}; // Tabelas public.* const tableRe = /CREATE TABLE (public\.\S+)\s*\(([\s\S]*?)\);/gm; let m; while ((m = tableRe.exec(content)) !== null) { const name = m[1].replace('public.', ''); const body = m[2]; const columns = []; for (let line of body.split('\n')) { line = line.trim().replace(/,$/, ''); if (!line || line.startsWith('--')) continue; if (/^(CONSTRAINT|PRIMARY KEY|UNIQUE|CHECK|FOREIGN KEY|EXCLUDE)/i.test(line)) continue; const col = line.match( /^(\w+)\s+([\w\[\]"()\s,]+?)(?:\s+DEFAULT\s+|\s+NOT NULL|\s+NULL|\s+GENERATED|\s+REFERENCES\s|$)/ ); if (col) { columns.push({ name: col[1], type: col[2].trim().split('(')[0].trim(), pk: col[1] === 'id', }); } } tables[name] = { columns, fks: [] }; } // FKs via ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY const fkRe = /ALTER TABLE ONLY public\.(\w+)\s+ADD CONSTRAINT \S+ FOREIGN KEY \((\w+)\) REFERENCES public\.(\w+)\((\w+)\)/gm; while ((m = fkRe.exec(content)) !== null) { const [, fromTable, fromCol, toTable, toCol] = m; if (tables[fromTable]) { tables[fromTable].fks.push({ from_col: fromCol, to_table: toTable, to_col: toCol }); } } // Views const viewRe = /CREATE(?:\s+OR REPLACE)?\s+VIEW\s+public\.(\S+)\s+AS/gm; const views = []; while ((m = viewRe.exec(content)) !== null) views.push(m[1]); return { tables, views }; } // --------------------------------------------------------------------------- // 3. Monta os domínios // Tabelas novas que ainda não estão mapeadas vão para "Outros" // --------------------------------------------------------------------------- function buildDomains(tables) { const mapped = new Set(Object.values(DOMAIN_TABLES).flat()); const others = Object.keys(tables).filter(t => !mapped.has(t)); const domains = {}; for (const [domain, list] of Object.entries(DOMAIN_TABLES)) { const present = list.filter(t => tables[t]); if (present.length > 0) domains[domain] = present; } if (others.length > 0) { domains['Outros'] = others; DOMAIN_COLORS['Outros'] = '#6b7280'; } return domains; } // --------------------------------------------------------------------------- // 4. Gera o HTML final (standalone, sem dependências externas de JS) // --------------------------------------------------------------------------- function generateHTML(tables, views, domains, date, available) { const totalFKs = Object.values(tables).reduce((a, t) => a + t.fks.length, 0); const totalCols = Object.values(tables).reduce((a, t) => a + t.columns.length, 0); const generated = new Date().toLocaleString('pt-BR'); // Serializa dados para embutir no HTML const jsonData = JSON.stringify({ tables, views, domains }); const jsonColors = JSON.stringify(DOMAIN_COLORS); return ` AgenciaPsi DB · ${date}
AgênciaPsi DB
${date} · ${generated}
${Object.keys(tables).length} tabelas
${totalFKs} FKs
${views.length} views
${totalCols} colunas
`; } // --------------------------------------------------------------------------- // 5. Execução // --------------------------------------------------------------------------- console.log('\n═══ AgenciaPsi — Dashboard Generator ═══\n'); const { schemaPath, date, available } = resolveSchema(); console.log(` → Schema: ${schemaPath}`); if (available.length > 1) console.log(` → Outros backups: ${available.slice(1).join(', ')}`); const content = fs.readFileSync(schemaPath, 'utf8'); console.log(` → Lendo schema... (${(content.length / 1024).toFixed(0)} KB)`); const { tables, views } = parseSchema(content); const domains = buildDomains(tables); const totalFKs = Object.values(tables).reduce((a, t) => a + t.fks.length, 0); console.log(` → ${Object.keys(tables).length} tabelas · ${totalFKs} FKs · ${views.length} views`); // Avisa sobre tabelas novas não mapeadas if (domains['Outros']) { console.log(`\n ⚠ Tabelas novas sem domínio definido (aparecerão em "Outros"):`); domains['Outros'].forEach(t => console.log(` - ${t}`)); console.log(` → Edite DOMAIN_TABLES no script para mapeá-las.\n`); } const html = generateHTML(tables, views, domains, date, available); fs.writeFileSync(OUTPUT_FILE, html, 'utf8'); console.log(`\n✔ Gerado: ${OUTPUT_FILE}`); console.log(` Tamanho: ${(fs.statSync(OUTPUT_FILE).size / 1024).toFixed(0)} KB`); console.log(` Abra no browser: file://${OUTPUT_FILE}\n`);