import { getWasm } from '../wasm.js';
import { esc } from '../utils/html.js';
import { createExportBar } from '../utils/export.js';
import { renderIndexTable } from '../utils/health-ui.js';
import { trackFeatureUse } from '../utils/analytics.js';
export function createAudit(container, files) {
container.innerHTML = `
<div class="flex-1 flex items-center justify-center p-12">
<div class="text-center">
<div class="inline-block w-8 h-8 border-2 border-innodb-cyan border-t-transparent rounded-full animate-spin mb-4"></div>
<p class="text-gray-400">Analyzing ${files.length} files\u2026</p>
</div>
</div>`;
requestAnimationFrame(() => {
buildAuditDashboard(container, files);
});
}
function buildAuditDashboard(container, files) {
const wasm = getWasm();
const results = [];
for (const file of files) {
const entry = { name: file.name, error: null, checksums: null, health: null };
try {
entry.checksums = JSON.parse(wasm.validate_checksums(file.data));
} catch (e) {
entry.error = String(e);
}
try {
entry.health = JSON.parse(wasm.analyze_health(file.data));
} catch {
}
results.push(entry);
}
let totalFiles = results.length;
let totalPages = 0;
let totalCorrupt = 0;
let totalValid = 0;
let totalEmpty = 0;
let fillFactors = [];
let fragValues = [];
for (const r of results) {
if (r.checksums) {
totalPages += r.checksums.total_pages;
totalCorrupt += r.checksums.invalid_pages;
totalValid += r.checksums.valid_pages;
totalEmpty += r.checksums.empty_pages;
}
if (r.health) {
fillFactors.push(r.health.summary.avg_fill_factor);
fragValues.push(r.health.summary.avg_fragmentation);
}
}
const nonEmpty = totalPages - totalEmpty;
const integrityPct = nonEmpty > 0 ? Math.min(((nonEmpty - totalCorrupt) / nonEmpty) * 100, 100).toFixed(1) : '100.0';
const avgFill = fillFactors.length > 0 ? (fillFactors.reduce((a, b) => a + b, 0) / fillFactors.length * 100).toFixed(1) : 'N/A';
const avgFrag = fragValues.length > 0 ? (fragValues.reduce((a, b) => a + b, 0) / fragValues.length * 100).toFixed(1) : 'N/A';
let sortCol = null;
let sortAsc = true;
let filterName = '';
let filterStatus = '';
container.innerHTML = `
<div class="p-6 space-y-6 overflow-auto max-h-full">
<div class="flex items-center gap-3">
<h2 class="text-lg font-bold text-innodb-cyan">Audit Dashboard</h2>
<span class="text-xs text-gray-500">${totalFiles} file${totalFiles !== 1 ? 's' : ''}</span>
<span id="audit-export"></span>
</div>
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
${statCard('Total Files', totalFiles)}
${statCard('Total Pages', totalPages)}
${statCard('Corrupt Pages', totalCorrupt, totalCorrupt > 0 ? 'text-innodb-red' : '')}
${statCard('Integrity', integrityPct + '%', parseFloat(integrityPct) >= 100 ? 'text-innodb-green' : parseFloat(integrityPct) >= 95 ? 'text-innodb-amber' : 'text-innodb-red')}
${statCard('Avg Fill Factor', avgFill !== 'N/A' ? avgFill + '%' : avgFill, avgFill !== 'N/A' ? 'text-innodb-cyan' : '')}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-surface-2 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
<span class="text-sm text-gray-400">Overall Integrity</span>
<span class="text-sm font-bold ${parseFloat(integrityPct) >= 100 ? 'text-innodb-green' : parseFloat(integrityPct) >= 95 ? 'text-innodb-amber' : 'text-innodb-red'}">${integrityPct}%</span>
</div>
<div class="w-full bg-gray-800 rounded-full h-3 overflow-hidden">
<div class="h-full rounded-full ${parseFloat(integrityPct) >= 100 ? 'bg-innodb-green' : parseFloat(integrityPct) >= 95 ? 'bg-innodb-amber' : 'bg-innodb-red'}" style="width:${Math.min(parseFloat(integrityPct), 100)}%"></div>
</div>
</div>
<div class="bg-surface-2 rounded-lg p-4">
<div class="flex items-center gap-2 mb-2">
<span class="text-sm text-gray-400">Avg Fragmentation</span>
<span class="text-sm font-bold text-gray-300">${avgFrag !== 'N/A' ? avgFrag + '%' : avgFrag}</span>
</div>
<div class="w-full bg-gray-800 rounded-full h-3 overflow-hidden">
<div class="h-full rounded-full bg-innodb-amber" style="width:${avgFrag !== 'N/A' ? Math.min(parseFloat(avgFrag), 100) : 0}%"></div>
</div>
</div>
</div>
<h3 class="text-md font-semibold text-gray-300">Per-File Details</h3>
<div class="flex items-center gap-3">
<input id="audit-filter-name" type="text" placeholder="Filter by file name\u2026"
class="px-2 py-1 bg-surface-3 border border-gray-700 rounded text-xs text-gray-300 placeholder-gray-600 w-48 focus:outline-none focus:border-innodb-cyan" />
<select id="audit-filter-status"
class="px-2 py-1 bg-surface-3 border border-gray-700 rounded text-xs text-gray-300 focus:outline-none focus:border-innodb-cyan">
<option value="">All statuses</option>
<option value="healthy">Healthy</option>
<option value="warning">Warning</option>
<option value="critical">Critical</option>
<option value="error">Error</option>
</select>
</div>
<div id="audit-table-wrap" class="overflow-x-auto max-h-96"></div>
</div>
`;
const exportSlot = container.querySelector('#audit-export');
if (exportSlot) {
const exportData = () => ({
summary: { totalFiles, totalPages, totalCorrupt, totalValid, totalEmpty, integrityPct, avgFill, avgFrag },
files: results.map((r) => ({
name: r.name,
error: r.error,
pages: r.checksums?.total_pages ?? 0,
corrupt: r.checksums?.invalid_pages ?? 0,
valid: r.checksums?.valid_pages ?? 0,
empty: r.checksums?.empty_pages ?? 0,
fill_factor: r.health?.summary.avg_fill_factor ?? null,
fragmentation: r.health?.summary.avg_fragmentation ?? null,
index_count: r.health?.summary.index_count ?? null,
})),
});
exportSlot.appendChild(createExportBar(exportData, 'audit'));
}
const fileRows = results.map((r) => {
const pages = r.checksums?.total_pages ?? 0;
const corrupt = r.checksums?.invalid_pages ?? 0;
const empty = r.checksums?.empty_pages ?? 0;
const nonEmpty = pages - empty;
const corruptPct = nonEmpty > 0 ? (corrupt / nonEmpty) * 100 : 0;
const fill = r.health?.summary.avg_fill_factor ?? null;
const frag = r.health?.summary.avg_fragmentation ?? null;
const indexCount = r.health?.summary.index_count ?? null;
const indexes = r.health?.indexes ?? null;
return { name: r.name, pages, corrupt, corruptPct, fill, frag, indexCount, indexes, error: r.error };
});
const expandedNames = new Set();
function getRowStatus(r) {
if (r.error) return 'error';
if (r.corruptPct === 0) return 'healthy';
if (r.corruptPct < 5) return 'warning';
return 'critical';
}
function renderTable() {
let sorted = fileRows.filter((r) => {
if (filterName && !r.name.toLowerCase().includes(filterName.toLowerCase())) return false;
if (filterStatus && getRowStatus(r) !== filterStatus) return false;
return true;
});
sorted = [...sorted];
if (sortCol !== null) {
sorted.sort((a, b) => {
let va = a[sortCol];
let vb = b[sortCol];
if (va == null) va = -Infinity;
if (vb == null) vb = -Infinity;
if (typeof va === 'string') return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
return sortAsc ? va - vb : vb - va;
});
}
const columns = [
{ key: 'name', label: 'File' },
{ key: 'pages', label: 'Pages' },
{ key: 'corrupt', label: 'Corrupt' },
{ key: 'corruptPct', label: 'Status' },
{ key: 'fill', label: 'Fill Factor' },
{ key: 'frag', label: 'Fragmentation' },
{ key: 'indexCount', label: 'Indexes' },
];
const colCount = columns.length;
const wrap = container.querySelector('#audit-table-wrap');
wrap.innerHTML = `
<table class="w-full text-xs font-mono">
<thead class="sticky top-0 bg-gray-950">
<tr class="text-left text-gray-500 border-b border-gray-800">
${columns.map((c) => `<th scope="col" class="py-1 pr-3 cursor-pointer hover:text-gray-300 select-none" data-sort="${c.key}">${c.label}${sortCol === c.key ? (sortAsc ? ' ▲' : ' ▼') : ''}</th>`).join('')}
</tr>
</thead>
<tbody>
${sorted.map((r) => fileRow(r, colCount, expandedNames)).join('')}
</tbody>
</table>`;
wrap.querySelectorAll('th[data-sort]').forEach((th) => {
th.addEventListener('click', () => {
const col = th.dataset.sort;
if (sortCol === col) {
sortAsc = !sortAsc;
} else {
sortCol = col;
sortAsc = true;
}
renderTable();
});
});
wrap.querySelectorAll('.audit-expand-toggle').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const rowName = btn.dataset.rowName;
const detailRow = wrap.querySelector(`[data-detail-name="${CSS.escape(rowName)}"]`);
if (!detailRow) return;
const isExpanded = !detailRow.classList.contains('hidden');
if (isExpanded) {
detailRow.classList.add('hidden');
expandedNames.delete(rowName);
btn.innerHTML = expandArrow(false);
} else {
detailRow.classList.remove('hidden');
expandedNames.add(rowName);
btn.innerHTML = expandArrow(true);
}
});
});
}
renderTable();
const nameInput = container.querySelector('#audit-filter-name');
const statusSelect = container.querySelector('#audit-filter-status');
if (nameInput) {
nameInput.addEventListener('input', () => {
trackFeatureUse('audit_filter', { type: 'name' });
filterName = nameInput.value;
renderTable();
});
}
if (statusSelect) {
statusSelect.addEventListener('change', () => {
trackFeatureUse('audit_filter', { type: 'status', value: statusSelect.value });
filterStatus = statusSelect.value;
renderTable();
});
}
}
function expandArrow(expanded) {
return expanded
? '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg>'
: '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>';
}
function fileRow(r, colCount, expandedNames) {
let statusClass, statusText;
if (r.error) {
statusClass = 'text-gray-500';
statusText = 'error';
} else if (r.corruptPct === 0) {
statusClass = 'text-innodb-green';
statusText = 'healthy';
} else if (r.corruptPct < 5) {
statusClass = 'text-innodb-amber';
statusText = 'warning';
} else {
statusClass = 'text-innodb-red';
statusText = 'critical';
}
const dot = r.error ? 'bg-gray-500' : r.corruptPct === 0 ? 'bg-innodb-green' : r.corruptPct < 5 ? 'bg-innodb-amber' : 'bg-innodb-red';
const hasIndexes = r.indexes && r.indexes.length > 0;
const isExpanded = hasIndexes && expandedNames.has(r.name);
let html = `
<tr class="border-b border-gray-800/30 hover:bg-surface-2/50">
<td class="py-1 pr-3 text-gray-300">
<span class="inline-flex items-center gap-1.5">
${hasIndexes ? `<button class="audit-expand-toggle text-gray-500 hover:text-gray-300" data-row-name="${esc(r.name)}">${expandArrow(isExpanded)}</button>` : '<span class="w-3.5"></span>'}
${esc(r.name)}
</span>
</td>
<td class="py-1 pr-3 text-gray-400">${r.pages}</td>
<td class="py-1 pr-3 ${r.corrupt > 0 ? 'text-innodb-red font-bold' : 'text-gray-400'}">${r.corrupt}</td>
<td class="py-1 pr-3">
<span class="inline-flex items-center gap-1.5">
<span class="w-2 h-2 rounded-full ${dot}"></span>
<span class="${statusClass} font-bold">${statusText}</span>
</span>
</td>
<td class="py-1 pr-3 text-gray-400">${r.fill != null ? (r.fill * 100).toFixed(1) + '%' : '\u2014'}</td>
<td class="py-1 pr-3 text-gray-400">${r.frag != null ? (r.frag * 100).toFixed(1) + '%' : '\u2014'}</td>
<td class="py-1 pr-3 text-gray-400">${r.indexCount != null ? r.indexCount : '\u2014'}</td>
</tr>`;
if (hasIndexes) {
html += `
<tr data-detail-name="${esc(r.name)}" class="${isExpanded ? '' : 'hidden'}">
<td colspan="${colCount}" class="py-2 px-4 bg-surface-3/30">
<div class="text-xs text-gray-500 mb-1">Per-Index Health for ${esc(r.name)}</div>
<div class="overflow-x-auto">${renderIndexTable(r.indexes)}</div>
</td>
</tr>`;
}
return html;
}
function statCard(label, value, colorClass = '') {
return `
<div class="bg-surface-2 rounded-lg p-4">
<div class="text-xs text-gray-500 uppercase tracking-wide">${esc(label)}</div>
<div class="text-lg font-bold ${colorClass || 'text-gray-100'} mt-1">${value}</div>
</div>`;
}