import { getWasm } from '../wasm.js';
import { esc } from '../utils/html.js';
import { createExportBar } from '../utils/export.js';
import { fillFactorClass, bloatGradeColor } from '../utils/health-ui.js';
import { requestIndexFilter, navigateToTab } from '../utils/navigation.js';
import { createBTree } from './btree.js';
import { trackFeatureUse } from '../utils/analytics.js';
export function createHealth(container, fileData) {
const wasm = getWasm();
let report;
try {
if (wasm.analyze_health_extended) {
report = JSON.parse(wasm.analyze_health_extended(fileData, true, true, 30));
} else {
report = JSON.parse(wasm.analyze_health(fileData));
}
} catch (e) {
container.innerHTML = `<div class="p-6 text-red-400">Error analyzing health: ${esc(String(e))}</div>`;
return;
}
const indexNames = buildIndexNameMap(wasm, fileData);
const s = report.summary;
const avgFillPct = (s.avg_fill_factor * 100).toFixed(1);
const avgFragPct = (s.avg_fragmentation * 100).toFixed(1);
const avgGarbagePct = (s.avg_garbage_ratio * 100).toFixed(1);
const indexes = (report.indexes || []).map((idx) => ({
...idx,
_name: indexNames.get(idx.index_id) || idx.index_name || null,
_bloat_score: idx.bloat?.score ?? null,
_cardinality: idx.cardinality?.estimated_distinct ?? null,
}));
let sortCol = null;
let sortAsc = true;
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">B+Tree Health</h2>
<span class="text-xs text-gray-500">${s.index_count} index${s.index_count !== 1 ? 'es' : ''}</span>
<span id="health-export"></span>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
${statCard('Index Count', s.index_count)}
${statCard('Avg Fill Factor', avgFillPct + '%', fillFactorClass(s.avg_fill_factor))}
${statCard('Avg Fragmentation', avgFragPct + '%', 'text-innodb-amber')}
${(() => {
const worstBloat = indexes.reduce((worst, i) => {
if (!i.bloat) return worst;
return (!worst || i.bloat.score > worst.score) ? i.bloat : worst;
}, null);
if (worstBloat) {
return statCard('Worst Bloat', worstBloat.grade + ' (' + worstBloat.score.toFixed(2) + ')', bloatGradeColor(worstBloat.grade));
}
return statCard('Avg Garbage Ratio', avgGarbagePct + '%', 'text-gray-300');
})()}
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
${statCard('Total Pages', s.total_pages)}
${statCard('Index Pages', s.index_pages)}
${statCard('Non-Index Pages', s.non_index_pages)}
${statCard('Empty Pages', s.empty_pages)}
</div>
<div class="flex items-center gap-3">
<h3 class="text-md font-semibold text-gray-300">Per-Index Details</h3>
<span class="text-xs text-gray-600">Click a row to filter in Pages tab</span>
</div>
<div id="health-index-table" class="overflow-x-auto max-h-96"></div>
<div>
<button id="health-btree-toggle" class="px-3 py-1.5 bg-surface-3 hover:bg-gray-600 text-gray-300 rounded text-xs">
Show B+Tree
</button>
<div id="health-btree-container" class="hidden mt-4"></div>
</div>
</div>
`;
const exportSlot = container.querySelector('#health-export');
if (exportSlot) {
exportSlot.appendChild(createExportBar(() => report, 'health'));
}
function renderTable() {
let sorted = [...indexes];
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 wrap = container.querySelector('#health-index-table');
const hasBloat = indexes.some((i) => i.bloat);
const hasCard = indexes.some((i) => i.cardinality);
const colDefs = [
{ key: 'index_id', label: 'Index ID' },
{ key: '_name', label: 'Name' },
{ key: 'tree_depth', label: 'Depth' },
{ key: 'leaf_pages', label: 'Leaf Pages' },
{ key: 'total_records', label: 'Records' },
{ key: 'avg_fill_factor', label: 'Avg Fill' },
{ key: 'min_fill_factor', label: 'Min Fill' },
{ key: 'max_fill_factor', label: 'Max Fill' },
{ key: 'fragmentation', label: 'Frag %' },
{ key: 'avg_garbage_ratio', label: 'Garbage %' },
{ key: 'empty_leaf_pages', label: 'Empty Leaves' },
...(hasBloat ? [{ key: '_bloat_score', label: 'Bloat' }] : []),
...(hasCard ? [{ key: '_cardinality', label: 'Est. Cardinality' }] : []),
];
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">
${colDefs.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((idx) => healthIndexRow(idx)).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('tr[data-index-id]').forEach((tr) => {
tr.classList.add('cursor-pointer');
tr.addEventListener('click', () => {
const indexId = parseInt(tr.dataset.indexId, 10);
trackFeatureUse('health_index_click', { index_id: indexId });
requestIndexFilter(indexId);
navigateToTab('pages');
});
});
}
renderTable();
const btreeBtn = container.querySelector('#health-btree-toggle');
const btreeContainer = container.querySelector('#health-btree-container');
let btreeLoaded = false;
btreeBtn.addEventListener('click', () => {
if (!btreeLoaded) {
try {
const pagesData = JSON.parse(wasm.analyze_pages(fileData, -1n));
createBTree(btreeContainer, fileData, pagesData);
btreeLoaded = true;
} catch (e) {
btreeContainer.innerHTML = `<div class="p-4 text-red-400 text-sm">Error loading B+Tree: ${esc(String(e))}</div>`;
}
}
const isHidden = btreeContainer.classList.toggle('hidden');
btreeBtn.textContent = isHidden ? 'Show B+Tree' : 'Hide B+Tree';
if (!isHidden) trackFeatureUse('btree_toggle');
});
}
function buildIndexNameMap(wasm, fileData) {
const map = new Map();
try {
const raw = wasm.extract_sdi(fileData);
const records = JSON.parse(raw);
for (const rec of records) {
const dd = rec.dd_object;
if (!dd || !dd.indexes) continue;
for (const idx of dd.indexes) {
const match = (idx.se_private_data || '').match(/id=(\d+)/);
if (match && idx.name) {
map.set(parseInt(match[1], 10), idx.name);
}
}
}
} catch {
}
return map;
}
function healthIndexRow(idx) {
const avg = idx.avg_fill_factor ?? 0;
const min = idx.min_fill_factor ?? 0;
const max = idx.max_fill_factor ?? 0;
const frag = idx.fragmentation ?? 0;
const garbage = idx.avg_garbage_ratio ?? 0;
return `
<tr class="border-b border-gray-800/30 hover:bg-surface-2/50" data-index-id="${idx.index_id}">
<td class="py-1 pr-3 text-innodb-cyan">${idx.index_id}</td>
<td class="py-1 pr-3 text-gray-300">${idx._name ? esc(idx._name) : '\u2014'}</td>
<td class="py-1 pr-3 text-gray-400">${idx.tree_depth ?? '\u2014'}</td>
<td class="py-1 pr-3 text-gray-400">${idx.leaf_pages ?? '\u2014'}</td>
<td class="py-1 pr-3 text-gray-400">${idx.total_records ?? '\u2014'}</td>
<td class="py-1 pr-3">
<div class="flex items-center gap-2">
<span class="${fillFactorClass(avg)}">${(avg * 100).toFixed(1)}%</span>
<div class="w-16"><div class="w-full bg-gray-800 rounded-full h-2.5"><div class="${avg >= 0.70 ? 'bg-innodb-green' : avg >= 0.40 ? 'bg-innodb-amber' : 'bg-innodb-red'} h-full rounded-full" style="width:${(avg * 100).toFixed(1)}%"></div></div></div>
</div>
</td>
<td class="py-1 pr-3">
<div class="flex items-center gap-2">
<span class="${fillFactorClass(min)}">${(min * 100).toFixed(1)}%</span>
<div class="w-16"><div class="w-full bg-gray-800 rounded-full h-2.5"><div class="${min >= 0.70 ? 'bg-innodb-green' : min >= 0.40 ? 'bg-innodb-amber' : 'bg-innodb-red'} h-full rounded-full" style="width:${(min * 100).toFixed(1)}%"></div></div></div>
</div>
</td>
<td class="py-1 pr-3">
<div class="flex items-center gap-2">
<span class="${fillFactorClass(max)}">${(max * 100).toFixed(1)}%</span>
<div class="w-16"><div class="w-full bg-gray-800 rounded-full h-2.5"><div class="${max >= 0.70 ? 'bg-innodb-green' : max >= 0.40 ? 'bg-innodb-amber' : 'bg-innodb-red'} h-full rounded-full" style="width:${(max * 100).toFixed(1)}%"></div></div></div>
</div>
</td>
<td class="py-1 pr-3 text-gray-400">${(frag * 100).toFixed(1)}%</td>
<td class="py-1 pr-3 text-gray-400">${(garbage * 100).toFixed(1)}%</td>
<td class="py-1 pr-3 text-gray-400">${idx.empty_leaf_pages ?? 0}</td>
${idx.bloat ? `<td class="py-1 pr-3"><span class="${bloatGradeColor(idx.bloat.grade)} font-bold">${esc(idx.bloat.grade)}</span> <span class="text-gray-500">(${idx.bloat.score.toFixed(2)})</span></td>` : ''}
${idx.cardinality ? `<td class="py-1 pr-3 text-gray-400">~${idx.cardinality.estimated_distinct.toLocaleString()}</td>` : (idx._cardinality === null && idx.bloat ? '<td class="py-1 pr-3 text-gray-500">\u2014</td>' : '')}
</tr>`;
}
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>`;
}