@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
JavaScript
"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