UNPKG

@vasoyaprince14/sql-analyzer

Version:

๐Ÿš€ Enhanced SQL database analyzer with AI-powered insights, comprehensive security analysis, RLS policy auditing, and beautiful HTML reports

1,034 lines (940 loc) โ€ข 141 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EnhancedReportGenerator = void 0; const chalk_1 = __importDefault(require("chalk")); class EnhancedReportGenerator { getQuickFixes(report) { const fixes = []; // Security first (group RLS to avoid repetition) const rlsTables = []; const otherVulns = []; report.securityAnalysis.vulnerabilities.forEach(v => { if (v.type === 'rls_disabled') { rlsTables.push(...(v.affectedObjects || [])); } else { otherVulns.push(v); } }); if (rlsTables.length > 0) { const unique = Array.from(new Set(rlsTables)); fixes.push({ severity: 'high', title: `Enable RLS on ${unique.length} table(s)`, description: `Tables: ${unique.slice(0, 10).join(', ')}${unique.length > 10 ? 'โ€ฆ' : ''}`, sql: unique.map(t => `ALTER TABLE ${t} ENABLE ROW LEVEL SECURITY;`).join('\n') }); } otherVulns.forEach(v => { const sev = v.severity; fixes.push({ severity: sev, title: v.description, description: v.impact, sql: v.solution && (v.solution.includes('ALTER') || v.solution.includes('REVOKE')) ? v.solution : undefined }); }); // Missing FK indexes report.schemaHealth.issues.filter(i => i.type === 'missing_fk_index').forEach(i => { fixes.push({ severity: 'high', title: `Index FK ${i.table}.${i.column}`, description: i.suggestion, sql: i.sqlFix }); }); // Missing PKs report.schemaHealth.issues.filter(i => i.type === 'missing_pk').forEach(i => { fixes.push({ severity: 'critical', title: `Add PK to ${i.table}`, description: i.suggestion, sql: i.sqlFix }); }); // Bloat quick action report.tableAnalysis.tablesWithBloat.slice(0, 5).forEach(t => { fixes.push({ severity: 'medium', title: `Repack/VACUUM ${t.tableName}`, description: `Reclaim ${t.wastedSpace}`, sql: `VACUUM (FULL, ANALYZE) ${t.tableName};` }); }); return fixes; } collectSqlFixes(report) { const sqls = []; report.schemaHealth.issues.forEach(i => { if (i.sqlFix) sqls.push(i.sqlFix); }); report.indexAnalysis.unusedIndexes.forEach(u => sqls.push(`DROP INDEX IF EXISTS ${u.name};`)); report.optimizationRecommendations.forEach(r => (r.sqlCommands || []).forEach(s => sqls.push(s))); report.tableAnalysis.tablesWithBloat.forEach(t => sqls.push(`VACUUM (FULL, ANALYZE) ${t.tableName};`)); report.securityAnalysis.vulnerabilities.forEach(v => { if (v.solution?.match(/ALTER|REVOKE/)) sqls.push(v.solution); }); return Array.from(new Set(sqls)); } getSqlFixesBySafety(report) { const all = this.collectSqlFixes(report); const destructivePatterns = [/\bDROP\b\s+INDEX/i, /\bDROP\b\s+TABLE/i, /\bTRUNCATE\b/i]; const destructive = []; const safe = []; all.forEach(sql => { if (destructivePatterns.some(rx => rx.test(sql))) destructive.push(sql); else safe.push(sql); }); return { safe, destructive }; } getStrategicRecommendations(report) { const recs = []; // Partitioning suggestion for very large tables const large = report.tableAnalysis.largeTables.filter(t => { const num = parseFloat(String(t.size).replace(/[^0-9.]/g, '')); return (t.size.includes('GB') && num >= 5) || (t.size.includes('TB')); }); if (large.length > 0) { recs.push(`Partition ${large.length} large table(s) by time/tenant to reduce bloat and speed maintenance.`); } // Connection pooling and locks if ((report.databaseInfo.connectionInfo.activeConnections / Math.max(report.databaseInfo.connectionInfo.maxConnections, 1)) > 0.7) { recs.push('Introduce PgBouncer and keep transactions short to reduce lock contention.'); } // Index strategy if (report.indexAnalysis.missingIndexes.length > 0) { recs.push('Define an indexing policy: always index FKs and frequent filter columns; audit quarterly.'); } // Security hardening const rlsMissing = report.securityAnalysis.vulnerabilities.filter(v => v.type === 'rls_disabled'); if (rlsMissing.length > 0) { recs.push('Adopt tenant/user isolation with RLS and role-based grants; avoid PUBLIC privileges.'); } // Maintenance automation if (report.tableAnalysis.tablesWithBloat.length > 0) { recs.push('Automate VACUUM/ANALYZE and schedule pg_repack for bloat-heavy tables.'); } return recs; } sanitizeAIText(input) { if (input == null) return ''; let text = String(input); // Strip fenced code blocks like ```json ... ``` and keep inner content text = text.replace(/```[a-zA-Z]*\n([\s\S]*?)```/g, '$1'); // Remove stray backticks text = text.replace(/```/g, ''); // Collapse excessive whitespace text = text.replace(/\s+\n/g, '\n').trim(); return text; } normalizeToArray(value) { if (!value) return []; if (Array.isArray(value)) { return value.map(v => this.sanitizeAIText(v)).filter(Boolean); } const text = this.sanitizeAIText(value); const lines = text .split(/\r?\n|\u2022|\-|\d+\.\s/) .map(s => s.trim()) .filter(s => s.length > 0); return lines.length ? lines : [text]; } generateTOC(report, isAI) { const items = [ { id: 'tldr', label: 'TL;DR' }, ...(isAI ? [{ id: 'ai-exec', label: 'AI Executive Summary' }] : []), { id: 'schema', label: 'Schema' }, { id: 'tables', label: 'Tables' }, { id: 'security', label: 'Security' }, { id: 'performance', label: 'Performance' }, { id: 'optimization', label: 'Optimization' }, { id: 'queries', label: 'Queries' }, { id: 'config', label: 'Configuration' }, { id: 'maintenance', label: 'Maintenance' }, { id: 'cost', label: 'Cost' }, ...(report.aiInsights ? [{ id: 'ai', label: 'AI Insights (Details)' }] : []), { id: 'trends', label: 'Trends' }, ]; return ` <nav class="toc" style="display:flex; flex-wrap:wrap; gap:10px; margin: 20px 0 10px;"> ${items.map(i => `<a href="#${i.id}" class="export-btn" style="padding:8px 14px; font-size: 0.9em;">${i.label}</a>`).join('')} </nav>`; } generateTLDR(report, quickFixes) { const criticalSec = report.securityAnalysis.vulnerabilities.filter(v => v.severity === 'critical').length; const bloatCount = report.tableAnalysis.tablesWithBloat.length; const missingIdx = report.indexAnalysis.missingIndexes.length; const savings = report.costAnalysis.optimizationSavings.monthly.toFixed(0); const trend = report.__trend; return ` <a id="tldr"></a> <div class="section"> <div class="section-header"><h2>๐Ÿงพ TL;DR</h2></div> <div class="section-content"> <ul style="margin-left:20px; list-style: disc;"> <li><strong>Overall:</strong> ${report.schemaHealth.overall.toFixed(1)}/10, <strong>Security issues:</strong> ${report.securityAnalysis.vulnerabilities.length}, <strong>Missing indexes:</strong> ${missingIdx}, <strong>Bloated tables:</strong> ${bloatCount}</li> <li><strong>Top risk:</strong> ${criticalSec > 0 ? criticalSec + ' critical security item(s)' : 'No critical security; address high/medium next'}</li> <li><strong>Potential savings:</strong> ~$${savings}/month after fixes</li> ${trend ? `<li><strong>Trend since last run:</strong> Overall ${trend.overallDelta >= 0 ? '+' : ''}${trend.overallDelta}/10, Security ${trend.securityDelta >= 0 ? '+' : ''}${trend.securityDelta}, Missing Idx ${trend.missingIdxDelta >= 0 ? '+' : ''}${trend.missingIdxDelta}, Bloat ${trend.bloatDelta >= 0 ? '+' : ''}${trend.bloatDelta}</li>` : ''} </ul> ${quickFixes.length ? `<h3 style="margin-top:15px">Quick Actions</h3><ol style="margin-left:20px;">${quickFixes.slice(0, 5).map(f => `<li>${f.title}</li>`).join('')}</ol>` : ''} <div class="filter-toolbar" style="margin-top:15px; display:flex; gap:10px; flex-wrap:wrap;"> <button class="export-btn" onclick="filterBySeverity('all')">All</button> <button class="export-btn" onclick="filterBySeverity('critical')">Critical</button> <button class="export-btn" onclick="filterBySeverity('high')">High</button> <button class="export-btn" onclick="filterBySeverity('medium')">Medium</button> <button class="export-btn" onclick="filterBySeverity('low')">Low</button> </div> </div> </div>`; } generateAIExecutiveSummary(aiInsights, report) { const priority = this.normalizeToArray(aiInsights?.priorityRecommendations); const roadmap = this.normalizeToArray(aiInsights?.implementationRoadmap); const impact = `$${report.costAnalysis?.optimizationSavings?.monthly?.toFixed ? report.costAnalysis.optimizationSavings.monthly.toFixed(0) : report.costAnalysis?.optimizationSavings?.monthly || 0}/mo`; const quickSql = this.collectSqlFixes(report).slice(0, 10); const quickFixCount = this.getQuickFixes(report).length; return ` <a id="ai-exec"></a> <div class="section" style="border:2px solid #6c5ce7;"> <div class="section-header" style="background:linear-gradient(135deg,#6c5ce7,#a29bfe)"><h2 style="color:white">๐Ÿค– Executive AI Summary</h2></div> <div class="section-content"> <div class="dashboard" style="margin-bottom:10px;"> <div class="metric-card"><div class="metric-icon">โšก</div><div class="metric-value">${priority.length || quickFixCount}</div><div class="metric-label">Top Actions</div></div> <div class="metric-card"><div class="metric-icon">๐Ÿ’ฐ</div><div class="metric-value">${impact}</div><div class="metric-label">Est. Monthly Savings</div></div> <div class="metric-card"><div class="metric-icon">๐Ÿ—“๏ธ</div><div class="metric-value">${Math.max(roadmap.length, 1)}w</div><div class="metric-label">Timeline</div></div> </div> <div class="issue-grid"> ${priority.length ? `<div class="issue-card high">` : ''} <div class="issue-header"><span class="severity-badge severity-high">PLAN</span><h3 class="issue-title">Action Matrix</h3></div> <div class="issue-content"> <table style="width:100%; border-collapse:collapse;"> <thead><tr><th style="text-align:left; padding:6px; border-bottom:1px solid #eee;">Priority</th><th style="text-align:left; padding:6px; border-bottom:1px solid #eee;">Action</th><th style="text-align:left; padding:6px; border-bottom:1px solid #eee;">Owner</th><th style="text-align:left; padding:6px; border-bottom:1px solid #eee;">ETA</th></tr></thead> <tbody> ${priority.slice(0, 8).map((p, i) => `<tr><td style="padding:6px;">P${i + 1}</td><td style="padding:6px;">${p}</td><td style="padding:6px;">DBA</td><td style="padding:6px;">${i < 3 ? 'This week' : 'Next'}</td></tr>`).join('')} </tbody> </table> </div> ${priority.length ? `</div>` : ''} <div class="issue-card medium"> <div class="issue-header"><span class="severity-badge severity-medium">DO</span><h3 class="issue-title">Copy-and-Run SQL</h3></div> <div class="issue-content"> ${quickSql.length ? `<div class="sql-code">${quickSql.join('\n')}</div>` : '<p>No automated SQL available.</p>'} </div> </div> ${roadmap.length ? `<div class="issue-card low">` : ''} <div class="issue-header"><span class="severity-badge severity-low">TRACK</span><h3 class="issue-title">Roadmap</h3></div> <div class="issue-content"> <div class="roadmap">${roadmap.map((r, i) => `<div class="roadmap-item"><div class="roadmap-week">Week ${i + 1}</div><div>${r}</div></div>`).join('')}</div> </div> ${roadmap.length ? `</div>` : ''} </div> </div> </div>`; } /** * Generate a modern, user-friendly HTML report */ generateEnhancedHTMLReport(report) { const templatesEnabled = false; const quickFixes = this.getQuickFixes(report); const allSql = this.collectSqlFixes(report); const isAI = Boolean(report.aiInsights); return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Database Health Audit Report</title> <style> /* Minimal inline icons via Unicode fallback; offline-friendly */ </style> <script> // Minimal inline Chart.js-like radar without external deps is complex; fallback to static numbers. // For offline mode, we skip external chart libs. Values still visible in cards. </script> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background: white; border-radius: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.1); overflow: hidden; } .header { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: white; padding: 40px 30px; text-align: center; position: relative; overflow: hidden; } .header::before { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>'); animation: float 20s infinite linear; } @keyframes float { 0% { transform: translate(-50%, -50%) rotate(0deg); } 100% { transform: translate(-50%, -50%) rotate(360deg); } } .header h1 { font-size: 3em; margin-bottom: 10px; position: relative; z-index: 1; } .header p { opacity: 0.9; font-size: 1.2em; position: relative; z-index: 1; } .header .timestamp { margin-top: 15px; font-size: 0.9em; opacity: 0.8; position: relative; z-index: 1; } .content { padding: 40px; } .dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 30px; margin-bottom: 40px; } .metric-card { background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); padding: 30px; border-radius: 15px; text-align: center; position: relative; overflow: hidden; transition: transform 0.3s ease, box-shadow 0.3s ease; } .metric-card:hover { transform: translateY(-5px); box-shadow: 0 15px 30px rgba(0,0,0,0.15); } .metric-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: linear-gradient(90deg, #667eea, #764ba2); } .metric-value { font-size: 3em; font-weight: bold; margin-bottom: 10px; background: linear-gradient(135deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .metric-label { color: #666; font-size: 1.1em; font-weight: 500; } .metric-icon { font-size: 2em; margin-bottom: 15px; color: #667eea; } .section { background: white; margin-bottom: 30px; border-radius: 15px; box-shadow: 0 5px 15px rgba(0,0,0,0.08); overflow: hidden; border: 1px solid #f0f0f0; } .section-header { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); padding: 25px 30px; border-bottom: 2px solid #e9ecef; } .section-header h2 { color: #495057; font-size: 1.8em; margin: 0; display: flex; align-items: center; } .section-header i { margin-right: 15px; color: #667eea; } .section-content { padding: 30px; } .score-display { display: flex; align-items: center; justify-content: center; margin: 20px 0; } .score-circle { position: relative; width: 120px; height: 120px; margin-right: 30px; } .score-number { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 2em; font-weight: bold; color: #333; } .score-description { flex: 1; font-size: 1.1em; color: #666; } .chart-container { margin: 30px 0; padding: 20px; background: #f8f9fa; border-radius: 10px; position: relative; height: 400px; } .issue-grid { display: grid; gap: 20px; margin-top: 20px; } .issue-card { padding: 20px; border-radius: 10px; border-left: 4px solid; background: white; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.2s ease; } .issue-card:hover { transform: translateX(5px); } .issue-card.critical { border-left-color: #dc3545; background: #fdf2f2; } .issue-card.high { border-left-color: #fd7e14; background: #fef8f3; } .issue-card.medium { border-left-color: #ffc107; background: #fffbf0; } .issue-card.low { border-left-color: #28a745; background: #f0f8f4; } .issue-header { display: flex; align-items: center; margin-bottom: 15px; } .severity-badge { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 0.8em; font-weight: bold; text-transform: uppercase; margin-right: 15px; } .severity-critical { background: #dc3545; color: white; } .severity-high { background: #fd7e14; color: white; } .severity-medium { background: #ffc107; color: #000; } .severity-low { background: #28a745; color: white; } .issue-title { font-size: 1.2em; font-weight: 600; color: #333; margin: 0; } .issue-content { margin-top: 15px; } .issue-section { margin-bottom: 15px; } .issue-section h4 { color: #555; margin-bottom: 8px; font-size: 1em; font-weight: 600; } .issue-section p { color: #666; line-height: 1.5; } .before-after { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px; padding: 20px; background: #f8f9fa; border-radius: 8px; } .before, .after { text-align: center; } .before h4 { color: #dc3545; margin-bottom: 10px; } .after h4 { color: #28a745; margin-bottom: 10px; } .improvement-badge { display: inline-block; background: linear-gradient(135deg, #28a745, #20c997); color: white; padding: 8px 16px; border-radius: 20px; font-size: 0.9em; font-weight: bold; margin-top: 10px; } .sql-code { background: #2d3748; color: #e2e8f0; padding: 15px 20px; border-radius: 8px; font-family: 'Monaco', 'Consolas', monospace; font-size: 0.9em; margin: 15px 0; overflow-x: auto; white-space: pre-wrap; border: 1px solid #4a5568; } .recommendation-grid { display: grid; gap: 25px; margin-top: 20px; } .recommendation-card { background: linear-gradient(135deg, #e7f3ff 0%, #f0f8ff 100%); padding: 25px; border-radius: 12px; border: 2px solid #b3d9ff; position: relative; } .recommendation-card::before { content: '๐Ÿ’ก'; position: absolute; top: 15px; right: 20px; font-size: 1.5em; } .recommendation-title { color: #0056b3; font-size: 1.3em; font-weight: 600; margin-bottom: 15px; } .implementation-time { display: inline-block; background: #0056b3; color: white; padding: 4px 12px; border-radius: 15px; font-size: 0.8em; margin-top: 10px; } .risk-indicator { display: inline-block; padding: 4px 12px; border-radius: 15px; font-size: 0.8em; margin-left: 10px; } .risk-low { background: #d4edda; color: #155724; } .risk-medium { background: #fff3cd; color: #856404; } .risk-high { background: #f8d7da; color: #721c24; } .ai-insights { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px; margin-top: 30px; } .ai-insights h3 { margin-bottom: 20px; font-size: 1.5em; } .insight-item { background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 8px; margin-bottom: 15px; backdrop-filter: blur(10px); } .roadmap { display: grid; gap: 15px; margin-top: 20px; } .roadmap-item { background: rgba(255, 255, 255, 0.1); padding: 15px; border-radius: 8px; border-left: 4px solid #fff; display: flex; align-items: center; } .roadmap-week { background: white; color: #667eea; padding: 8px 12px; border-radius: 20px; font-weight: bold; margin-right: 15px; font-size: 0.9em; } .footer { text-align: center; padding: 30px; background: #f8f9fa; color: #666; border-top: 1px solid #e9ecef; } .export-buttons { position: fixed; bottom: 30px; right: 30px; z-index: 1000; } .export-btn { display: block; background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 12px 20px; border-radius: 25px; text-decoration: none; margin-bottom: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.2); transition: transform 0.2s ease; } .export-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.3); } @media (max-width: 768px) { .dashboard { grid-template-columns: 1fr; } .before-after { grid-template-columns: 1fr; } .content { padding: 20px; } .export-buttons { position: relative; bottom: auto; right: auto; text-align: center; margin-top: 20px; } } .tooltip { position: relative; cursor: help; } .tooltip:hover::after { content: attr(data-tooltip); position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); background: #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 0.8em; white-space: nowrap; z-index: 1000; } </style> </head> <body> <div class="container"> <div class="header"> <h1><i class="fas fa-heartbeat"></i> Database Health Audit Report</h1> <p>Comprehensive analysis and optimization recommendations</p> <div class="timestamp"> Generated on ${new Date().toLocaleString()} | Database: ${report.databaseInfo.version.split(' ')[0]} </div> </div> <div class="content"> ${this.generateTOC(report, isAI)} ${this.generateTLDR(report, quickFixes)} ${isAI ? this.generateAIExecutiveSummary(report.aiInsights, report) : ''} <div class="section" style="margin-top:0"> <div class="section-header"> <h2>โšก At a Glance</h2> </div> <div class="section-content"> <div class="dashboard"> <div class="metric-card"><div class="metric-icon">๐Ÿฅ</div><div class="metric-value">${report.schemaHealth.overall.toFixed(1)}/10</div><div class="metric-label">Health</div></div> <div class="metric-card"><div class="metric-icon">๐Ÿ›ก๏ธ</div><div class="metric-value">${report.securityAnalysis.vulnerabilities.length}</div><div class="metric-label">Security Issues</div></div> <div class="metric-card"><div class="metric-icon">๐Ÿ—„๏ธ</div><div class="metric-value">${report.databaseInfo.tableCount}</div><div class="metric-label">Tables</div></div> <div class="metric-card"><div class="metric-icon">๐Ÿ”</div><div class="metric-value">${report.indexAnalysis.missingIndexes.length}</div><div class="metric-label">Missing Indexes</div></div> </div> <h3 style="margin-top:20px">๐ŸŽฏ Top Fixes</h3> ${quickFixes.length ? `<div class="issue-grid">${quickFixes.slice(0, 6).map(f => ` <div class="issue-card ${f.severity}"> <div class="issue-header"> <span class="severity-badge severity-${f.severity}">${f.severity.toUpperCase()}</span> <h3 class="issue-title">${f.title}</h3> </div> <div class="issue-content"> <p>${f.description}</p> ${f.sql ? `<div class="sql-code">${f.sql}</div>` : ''} </div> </div>`).join('')}</div>` : '<p>No immediate fixes detected.</p>'} ${allSql.length ? `<div style="margin-top:15px"><a class="export-btn" href="#" onclick='navigator.clipboard.writeText(${JSON.stringify(allSql.join("\n"))});return false;'>๐Ÿ“‹ Copy All SQL Fixes</a></div>` : ''} </div> </div> <!-- Executive Dashboard --> <div class="dashboard"> <div class="metric-card"> <div class="metric-icon"><i class="fas fa-database"></i></div> <div class="metric-value">${report.schemaHealth.overall.toFixed(1)}/10</div> <div class="metric-label">Overall Health Score</div> </div> <div class="metric-card"> <div class="metric-icon"><i class="fas fa-table"></i></div> <div class="metric-value">${report.databaseInfo.tableCount}</div> <div class="metric-label">Tables</div> </div> <div class="metric-card"> <div class="metric-icon"><i class="fas fa-list"></i></div> <div class="metric-value">${report.databaseInfo.indexCount}</div> <div class="metric-label">Indexes</div> </div> <div class="metric-card"> <div class="metric-icon"><i class="fas fa-shield-alt"></i></div> <div class="metric-value">${report.securityAnalysis.vulnerabilities.length}</div> <div class="metric-label">Security Issues</div> </div> <div class="metric-card"> <div class="metric-icon"><i class="fas fa-chart-line"></i></div> <div class="metric-value">${report.performanceIssues.length}</div> <div class="metric-label">Performance Issues</div> </div> <div class="metric-card"> <div class="metric-icon"><i class="fas fa-dollar-sign"></i></div> <div class="metric-value">$${report.costAnalysis.optimizationSavings.monthly.toFixed(0)}</div> <div class="metric-label">Monthly Savings Potential</div> </div> </div> <!-- Schema Health Breakdown --> <a id="schema"></a> <div class="section"> <div class="section-header"> <h2><i class="fas fa-project-diagram"></i> Schema Health Analysis</h2> <p class="section-description">Comprehensive analysis of your database schema using live system catalogs (information_schema/pg_stat). Each finding includes the reason and how to fix it.</p> </div> <div class="section-content"> <div class="chart-container"> <canvas id="schemaHealthChart"></canvas> </div> <!-- Detailed Metrics Dashboard --> <div class="dashboard" style="margin-top: 30px;"> <div class="metric-card"> <div class="metric-icon"><i class="fas fa-star"></i></div> <div class="metric-value">${report.schemaHealth.overall}/10</div> <div class="metric-label">Overall Score</div> <div class="metric-trend">${this.getScoreDescription(report.schemaHealth.overall)}</div> </div> <div class="metric-card"> <div class="metric-icon"><i class="fas fa-layer-group"></i></div> <div class="metric-value">${report.schemaHealth.normalization}/10</div> <div class="metric-label">Normalization</div> <div class="metric-trend">${this.getNormalizationDescription(report.schemaHealth.normalization)}</div> </div> <div class="metric-card"> <div class="metric-icon"><i class="fas fa-link"></i></div> <div class="metric-value">${report.schemaHealth.foreignKeyIntegrity}/10</div> <div class="metric-label">FK Integrity</div> <div class="metric-trend">${this.getFKDescription(report.schemaHealth.foreignKeyIntegrity)}</div> </div> <div class="metric-card"> <div class="metric-icon"><i class="fas fa-tags"></i></div> <div class="metric-value">${report.schemaHealth.naming}/10</div> <div class="metric-label">Naming</div> <div class="metric-trend">${this.getNamingDescription(report.schemaHealth.naming)}</div> </div> </div> <!-- Deep Dive Analysis --> <div class="analysis-deep-dive" style="margin-top: 40px;"> <h3><i class="fas fa-microscope"></i> Schema Health Deep Dive</h3> <!-- What is Schema Health --> <div class="analysis-section" style="margin-bottom: 30px; padding: 25px; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-radius: 12px; border-left: 5px solid #667eea;"> <h4><i class="fas fa-info-circle" style="color: #667eea;"></i> Understanding Schema Health</h4> <div class="explanation-box"> <p style="font-size: 1.1em; margin-bottom: 20px;"><strong>Schema health</strong> measures how well your database structure follows industry best practices, design patterns, and optimization principles. A healthy schema directly impacts your application's performance, maintainability, and scalability.</p> <div class="health-components" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-top: 25px;"> <div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);"> <h5 style="color: #667eea; margin-bottom: 15px;"><i class="fas fa-layer-group"></i> Normalization (${report.schemaHealth.normalization}/10)</h5> <p>How well your data is organized to minimize redundancy and improve consistency. Higher normalization reduces storage and improves data integrity.</p> <div style="margin-top: 10px; font-size: 0.9em; color: #666;"> <strong>Impact:</strong> Storage efficiency, data consistency, update performance </div> </div> <div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);"> <h5 style="color: #667eea; margin-bottom: 15px;"><i class="fas fa-link"></i> Foreign Key Integrity (${report.schemaHealth.foreignKeyIntegrity}/10)</h5> <p>Proper relationships and referential integrity constraints between tables. Ensures data consistency and prevents orphaned records.</p> <div style="margin-top: 10px; font-size: 0.9em; color: #666;"> <strong>Impact:</strong> Data consistency, query reliability, application stability </div> </div> <div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);"> <h5 style="color: #667eea; margin-bottom: 15px;"><i class="fas fa-tags"></i> Naming Conventions (${report.schemaHealth.naming}/10)</h5> <p>Consistent and meaningful naming patterns for tables, columns, and constraints. Improves code readability and team collaboration.</p> <div style="margin-top: 10px; font-size: 0.9em; color: #666;"> <strong>Impact:</strong> Code maintainability, team productivity, tool integration </div> </div> <div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);"> <h5 style="color: #667eea; margin-bottom: 15px;"><i class="fas fa-database"></i> Data Types (${report.schemaHealth.dataTypes}/10)</h5> <p>Appropriate data type usage for efficiency and storage optimization. Right-sized data types improve performance and reduce storage costs.</p> <div style="margin-top: 10px; font-size: 0.9em; color: #666;"> <strong>Impact:</strong> Storage efficiency, query performance, memory usage </div> </div> </div> </div> </div> <div class="analysis-section" style="margin: 30px 0; padding: 25px; background: #fff; border: 1px solid #eee; border-radius: 12px;"> <h4 style="margin-bottom: 15px;"><i class="fas fa-list-check"></i> Concrete Findings (Live)</h4> ${report.schemaHealth.issues.length > 0 ? ` <div class="issue-grid"> ${report.schemaHealth.issues.map(issue => ` <div class="issue-card ${issue.severity}"> <div class="issue-header"> <span class="severity-badge severity-${issue.severity}">${issue.severity.toUpperCase()}</span> <h3 class="issue-title">${issue.table}${issue.column ? '.' + issue.column : ''}: ${issue.type.replace('_', ' ')}</h3> </div> <div class="issue-content"> <div class="issue-section"> <h4>๐Ÿง  Why this is a problem</h4> <p>${issue.description}</p> </div> <div class="issue-section"> <h4>๐Ÿ›  How to improve</h4> <p>${issue.suggestion}</p> ${issue.sqlFix ? `<div class="sql-code">${issue.sqlFix}</div>` : ''} </div> <div class="before-after"> <div class="before"> <h4>Before</h4> <p>${issue.beforeFix || 'Less efficient or risky state'}</p> </div> <div class="after"> <h4>After</h4> <p>${issue.afterFix || 'Improved integrity or performance'}</p> <span class="improvement-badge">${issue.expectedImprovement || 'Measurable improvement'}</span> </div> </div> </div> </div> `).join('')} </div>` : '<p>No schema issues detected.</p>'} </div> <!-- Normalization Deep Dive --> <details class="analysis-section" style="margin-bottom: 30px; padding: 25px; background: linear-gradient(135deg, #f0f8ff 0%, #e6f3ff 100%); border-radius: 12px; border-left: 5px solid #007bff;"><summary style="cursor:pointer; font-weight:600; margin-bottom:10px;">Normalization Analysis (details)</summary> <h4><i class="fas fa-layer-group" style="color: #007bff;"></i> Database Normalization Analysis</h4> <div class="explanation-box"> <div class="score-explanation" style="background: white; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 2px solid #007bff;"> <div class="score-header" style="font-size: 1.2em; font-weight: bold; color: #007bff;"> Current Score: ${report.schemaHealth.normalization}/10 - ${this.getNormalizationDescription(report.schemaHealth.normalization)} </div> </div> <h5 style="color: #007bff; margin-bottom: 20px;">Understanding Database Normalization:</h5> <p style="margin-bottom: 25px;">Normalization is the process of organizing data in a database to reduce redundancy and improve data integrity. It involves decomposing tables to eliminate data redundancy and undesirable characteristics like insertion, update, and deletion anomalies.</p> <div class="normalization-levels" style="display: grid; gap: 25px;"> <div class="norm-level" style="background: white; padding: 20px; border-radius: 10px; box-shadow: 0 3px 15px rgba(0,123,255,0.1); border-left: 4px solid #007bff;"> <h6 style="color: #007bff; font-size: 1.1em; margin-bottom: 15px;"><strong>1NF (First Normal Form)</strong></h6> <ul style="margin-bottom: 15px; color: #333;"> <li>โœ“ Each column contains atomic (indivisible) values</li> <li>โœ“ Each record is unique</li> <li>โœ“ No repeating groups or arrays in columns</li> </ul> <div class="example" style="background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 3px solid #28a745;"> <strong style="color: #28a745;">โœ… Example - Moving to 1NF:</strong> <div class="code-block" style="background: #2d3748; color: #e2e8f0; padding: 15px; border-radius: 6px; margin-top: 10px; font-family: 'Courier New', monospace; font-size: 0.9em; overflow-x: auto;">-- โŒ Bad: Multiple values in one column (violates 1NF) CREATE TABLE users ( id SERIAL, name VARCHAR(100), phone_numbers TEXT -- "123-456-7890, 987-654-3210" ); -- โœ… Good: Separate table for multiple values (1NF compliant) CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(100) ); CREATE TABLE user_phones ( id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id), phone_number VARCHAR(20), phone_type VARCHAR(20) -- 'mobile', 'home', 'work' );</div> </div> </div> <div class="norm-level" style="background: white; padding: 20px; border-radius: 10px; box-shadow: 0 3px 15px rgba(0,123,255,0.1); border-left: 4px solid #17a2b8;"> <h6 style="color: #17a2b8; font-size: 1.1em; margin-bottom: 15px;"><strong>2NF (Second Normal Form)</strong></h6> <ul style="margin-bottom: 15px; color: #333;"> <li>โœ“ Must be in 1NF</li> <li>โœ“ No partial dependencies on composite primary keys</li> <li>โœ“ All non-key attributes depend on the entire primary key</li> </ul> <div class="example" style="background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 3px solid #28a745;"> <strong style="color: #28a745;">โœ… Example - Eliminating Partial Dependencies:</strong> ${templatesEnabled ? `<div class="code-block" style="background: #2d3748; color: #e2e8f0; padding: 15px; border-radius: 6px; margin-top: 10px; font-family: 'Courier New', monospace; font-size: 0.9em; overflow-x: auto;">${this.generateRealNormalizationExample(report)}</div>` : ''} </div> </div> <div class="norm-level" style="background: white; padding: 20px; border-radius: 10px; box-shadow: 0 3px 15px rgba(0,123,255,0.1); border-left: 4px solid #28a745;"> <h6 style="color: #28a745; font-size: 1.1em; margin-bottom: 15px;"><strong>3NF (Third Normal Form)</strong></h6> <ul style="margin-bottom: 15px; color: #333;"> <li>โœ“ Must be in 2NF</li> <li>โœ“ No transitive dependencies</li> <li>โœ“ Non-key attributes depend only on the primary key</li> </ul> <div class="example" style="background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 3px solid #28a745;"> <strong style="color: #28a745;">โœ… Example - Eliminating Transitive Dependencies:</strong> <div class="code-block" style="background: #2d3748; color: #e2e8f0; padding: 15px; border-radius: 6px; margin-top: 10px; font-family: 'Courier New', monospace; font-size: 0.9em; overflow-x: auto;">-- โŒ Bad: Transitive dependency (violates 3NF) CREATE TABLE employees ( id SERIAL PRIMARY KEY, name VARCHAR(100), department_id INT, department_name VARCHAR(100), -- Depends on department_id, not employee id department_budget DECIMAL(12,2) -- Also depends on department_id ); -- โœ… Good: Remove transitive dependencies (3NF compliant) CREATE TABLE departments ( id INT PRIMARY KEY, name VARCHAR(100), budget DECIMAL(12,2), manager_id INT ); CREATE TABLE employees ( id SERIAL PRIMARY KEY, name VARCHAR(100), department_id INT REFERENCES departments(id), hire_date DATE, salary DECIMAL(10,2) );</div> </div> </div> </div> <div style="margin-top: 30px; padding: 20px; background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%); border-radius: 10px; border-left: 4px solid #ffc107;"> <h5 style="color: #856404; margin-bottom: 15px;"><i class="fas fa-balance-scale"></i> Normalization Trade-offs:</h5> <div class="impact-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 15px;"> <div class="impact-positive" style="background: rgba(40, 167, 69, 0.1); padding: 15px; border-radius: 8px; border-left: 3px solid #28a745;"> <strong style="color: #28a745;"><i class="fas fa-check-circle"></i> Benefits</strong> <ul style="ma