#!/usr/bin/env node // ============================================================================= // AgenciaPsi — Dashboard Generator // ============================================================================= // Uso: // node generate-dashboard.cjs → usa backup mais recente // node generate-dashboard.cjs 2026-04-17 → usa backup de data específica // // Lê de: ./backups/YYYY-MM-DD/schema.sql // Lê de: ./db.config.json (domínios, cores e infraestrutura) // Gera: ./agenciapsi-db-dashboard.html (na mesma pasta do script) // ============================================================================= const fs = require('fs'); const path = require('path'); const ROOT = __dirname; const BACKUPS_DIR = path.join(ROOT, 'backups'); const OUTPUT_FILE = path.join(ROOT, 'agenciapsi-db-dashboard.html'); const CONFIG_FILE = path.join(ROOT, 'db.config.json'); // --------------------------------------------------------------------------- // Carrega config (domínios, cores e infraestrutura) // --------------------------------------------------------------------------- if (!fs.existsSync(CONFIG_FILE)) { console.error(`✖ Config não encontrada: ${CONFIG_FILE}`); process.exit(1); } const CONFIG = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); const DOMAIN_TABLES = CONFIG.domains || {}; const DOMAIN_COLORS = CONFIG.domainColors || {}; const INFRASTRUCTURE = CONFIG.infrastructure || {}; // --------------------------------------------------------------------------- // 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(` Rode primeiro: node db.cjs backup`); 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 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) && t !== '_db_migrations'); 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 infraGroups = Object.keys(INFRASTRUCTURE).length; const infraItems = Object.values(INFRASTRUCTURE).reduce((a, g) => a + (g.items?.length || 0), 0); const generated = new Date().toLocaleString('pt-BR'); // Slug por domínio — usado como id para scroll (ex: "SaaS / Planos" → "saas-planos") const slugify = (s) => s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); const domainSlugs = {}; for (const d of Object.keys(domains)) domainSlugs[d] = slugify(d); // Serializa dados para embutir no HTML const jsonData = JSON.stringify({ tables, views, domains, slugs: domainSlugs }); const jsonColors = JSON.stringify(DOMAIN_COLORS); const jsonInfra = JSON.stringify(INFRASTRUCTURE); return ` AgenciaPsi DB · ${date}
AgênciaPsi DB
${date} · ${generated}
${Object.keys(tables).length} tabelas
${totalFKs} FKs
${views.length} views
${totalCols} colunas
${infraItems} infra
`; } // --------------------------------------------------------------------------- // 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 "domains" em db.config.json para mapeá-las.\n`); } // Infra stats const infraGroups = Object.keys(INFRASTRUCTURE).length; const infraItems = Object.values(INFRASTRUCTURE).reduce((a, g) => a + (g.items?.length || 0), 0); console.log(` → Infraestrutura: ${infraGroups} grupos, ${infraItems} itens`); 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.replace(/\\/g, '/')}\n`);