Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 142 additions & 13 deletions 10-databases/visualizer/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@
};

let animating = false;
let pollTimer = null;

Check warning on line 147 in 10-databases/visualizer/app.js

View workflow job for this annotation

GitHub Actions / Frontend Linting

'pollTimer' is assigned a value but never used

function $(sel) { return document.querySelector(sel); }
function $$(sel) { return document.querySelectorAll(sel); }
Expand All @@ -159,6 +159,31 @@
return res.json();
}

// --- SQL Glass Box (auto-log queries to console) ---

function logSqlToConsole(result) {
const output = $('#console-output');
const steps = result.steps || [];

steps.forEach(step => {
if (!step.sql) return;
const qDiv = document.createElement('div');
qDiv.className = 'console-query';
qDiv.textContent = `mysql(${step.target})> ${step.sql}`;
output.appendChild(qDiv);

const rDiv = document.createElement('div');
rDiv.className = step.result.toLowerCase().includes('error') ||
step.result.toLowerCase().includes('not found') ||
step.result.toLowerCase().includes('stale')
? 'console-error' : 'console-meta';
rDiv.textContent = `-- ${step.result} (${step.latency_ms}ms)`;
output.appendChild(rDiv);
});

output.scrollTop = output.scrollHeight;
}

async function apiGet(url) {
const res = await fetch(url);
return res.json();
Expand Down Expand Up @@ -271,7 +296,7 @@

// --- Result Panel ---

function showResult(status, latency, data, cls) {
function showResult(status, latency, data, cls, apiResult) {
const panel = $('#result-panel');
const statusEl = $('#result-status');
const latencyEl = $('#result-latency');
Expand All @@ -282,6 +307,21 @@
latencyEl.textContent = `${latency.toFixed(1)}ms total`;
dataEl.textContent = JSON.stringify(data, null, 2);
panel.classList.remove('hidden');

// Show interpretation below the JSON
const existingInterp = panel.querySelector('.result-interpretation');
if (existingInterp) existingInterp.remove();
if (apiResult?.interpretation) {
const iDiv = document.createElement('div');
iDiv.className = 'result-interpretation';
iDiv.textContent = apiResult.interpretation;
panel.appendChild(iDiv);
}

// Log SQL queries to the console (without interpretation)
if (apiResult) {
logSqlToConsole(apiResult);
}
}

// --- Sidebar Update ---
Expand Down Expand Up @@ -368,7 +408,8 @@
lastStep.data?.lag_seconds === 0 ? 'REPLICATED' : 'LAG DETECTED',
result.total_ms,
result.steps.map(s => ({ action: s.action, result: s.result, ms: s.latency_ms })),
lastStep.data?.lag_seconds === 0 ? 'committed' : 'rolled-back'
lastStep.data?.lag_seconds === 0 ? 'committed' : 'rolled-back',
result
);

await updateSidebar();
Expand Down Expand Up @@ -433,7 +474,8 @@
result.outcome,
result.total_ms,
result.steps.map(s => ({ action: s.action, result: s.result, ms: s.latency_ms })),
committed ? 'committed' : 'rolled-back'
committed ? 'committed' : 'rolled-back',
result
);

await updateSidebar();
Expand Down Expand Up @@ -473,7 +515,8 @@
`Rows scanned: ${rows} | Key: ${keyUsed}`,
result.total_ms,
plan,
rows > 1000 ? 'rolled-back' : 'committed'
rows > 1000 ? 'rolled-back' : 'committed',
result
);

await updateSidebar();
Expand Down Expand Up @@ -508,7 +551,7 @@
if (replicaRect) replicaRect.style.opacity = '0.4';
showResult('PARTITION ACTIVE', result.latency_ms,
{ message: 'Replication stopped. Replica will not receive new writes.' },
'rolled-back');
'rolled-back', result);
await updateSidebar();
}

Expand All @@ -523,7 +566,7 @@
if (replicaRect) replicaRect.style.opacity = '1';
showResult('PARTITION RECOVERED', result.latency_ms,
{ message: 'Replication resumed. Replica is catching up.' },
'committed');
'committed', result);
await updateSidebar();
}

Expand Down Expand Up @@ -569,7 +612,8 @@
result.outcome,
result.total_ms,
result.steps.map(s => ({ action: s.action, result: s.result, ms: s.latency_ms })),
diverged ? 'rolled-back' : 'committed'
diverged ? 'rolled-back' : 'committed',
result
);

await updateSidebar();
Expand All @@ -588,7 +632,8 @@
showResult(
result.error ? 'ERROR' : `VIEW CREATED (${result.rows} rows)`,
result.latency_ms || 0, result,
result.error ? 'rolled-back' : 'committed'
result.error ? 'rolled-back' : 'committed',
result
);
await updateSidebar();
}
Expand Down Expand Up @@ -621,7 +666,8 @@
`JOIN: ${result.row_count} rows`,
result.total_ms, result.steps.map(s => ({
action: s.action, result: s.result, ms: s.latency_ms,
})), 'committed'
})), 'committed',
result
);

animating = false;
Expand Down Expand Up @@ -655,7 +701,8 @@
`VIEW: ${result.row_count} rows`,
result.total_ms, result.steps.map(s => ({
action: s.action, result: s.result, ms: s.latency_ms,
})), 'committed'
})), 'committed',
result
);

animating = false;
Expand All @@ -671,7 +718,8 @@
showResult(
result.error ? 'ERROR' : `VIEW REFRESHED (${result.rows} rows)`,
result.latency_ms || 0, result,
result.error ? 'rolled-back' : 'committed'
result.error ? 'rolled-back' : 'committed',
result
);
}

Expand All @@ -688,7 +736,7 @@
if (result.error) {
showResult('ERROR', 0, result, 'rolled-back');
} else {
showResult(`Buffer pool set to ${label}`, result.latency_ms, result, 'committed');
showResult(`Buffer pool set to ${label}`, result.latency_ms, result, 'committed', result);
}
}

Expand Down Expand Up @@ -730,7 +778,8 @@
memory_reads: s.memory_reads,
disk_reads: s.disk_reads,
},
s.buffer_hit_ratio > 90 ? 'committed' : 'rolled-back'
s.buffer_hit_ratio > 90 ? 'committed' : 'rolled-back',
result
);

await updateSidebar();
Expand Down Expand Up @@ -774,7 +823,7 @@

// --- SQL Console ---

let consoleHistory = [];

Check warning on line 826 in 10-databases/visualizer/app.js

View workflow job for this annotation

GitHub Actions / Frontend Linting

'consoleHistory' is never reassigned. Use 'const' instead
let consoleHistoryIndex = -1;

async function doSqlExec() {
Expand Down Expand Up @@ -891,6 +940,39 @@
$('#console-clear').addEventListener('click', () => {
$('#console-output').innerHTML = '';
});

// Draggable resize handle for bottom bar
const handle = $('#resize-handle');
const bar = document.querySelector('.bottom-bar');
if (handle && bar) {
let dragging = false;
let startY = 0;
let startH = 0;

handle.addEventListener('mousedown', (e) => {
dragging = true;
startY = e.clientY;
startH = bar.offsetHeight;
document.body.style.cursor = 'ns-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
});

document.addEventListener('mousemove', (e) => {
if (!dragging) return;
const diff = startY - e.clientY;
const newH = Math.max(120, Math.min(startH + diff, window.innerHeight * 0.7));
bar.style.height = newH + 'px';
});

document.addEventListener('mouseup', () => {
if (dragging) {
dragging = false;
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
});
}
}

function initButtons() {
Expand All @@ -917,10 +999,57 @@
pollTimer = setInterval(updateSidebar, POLL_INTERVAL_MS);
}

// --- Tooltips (JS-driven, fixed positioning) ---

function initTooltips() {
const popup = $('#tooltip-popup');
if (!popup) return;

document.addEventListener('mouseover', (e) => {
const el = e.target.closest('[data-tooltip]');
if (!el) {
popup.classList.remove('visible');
return;
}
popup.textContent = el.dataset.tooltip;
popup.classList.add('visible');

Comment on lines +1008 to +1016
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tooltips are only wired to mouseover/mouseout, so keyboard users (tabbing) won’t see them, and touch devices may not trigger them consistently. Consider adding focusin/focusout handling (and possibly aria-describedby or title fallback) so tooltips are accessible beyond mouse hover.

Copilot uses AI. Check for mistakes.
// Position above the element
const rect = el.getBoundingClientRect();
const popupW = popup.offsetWidth;
const popupH = popup.offsetHeight;
let left = rect.left + rect.width / 2 - popupW / 2;
let top = rect.top - popupH - 8;

// Keep within viewport
if (left < 8) left = 8;
if (left + popupW > window.innerWidth - 8) {
left = window.innerWidth - popupW - 8;
}
// If no room above, show below
if (top < 8) {
top = rect.bottom + 8;
}

popup.style.left = left + 'px';
popup.style.top = top + 'px';
});

document.addEventListener('mouseout', (e) => {
const el = e.target.closest('[data-tooltip]');
if (!el) return;
// Only hide if we're leaving the tooltip target
const related = e.relatedTarget;
if (related && el.contains(related)) return;
popup.classList.remove('visible');
});
}

document.addEventListener('DOMContentLoaded', () => {
initTabs();
initButtons();
initConsole();
initTooltips();
startPolling();
showExplanation('replication');
});
48 changes: 25 additions & 23 deletions 10-databases/visualizer/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
<body>
<header class="tab-bar">
<div class="tab-group">
<button class="tab active" data-tab="replication">Replication</button>
<button class="tab" data-tab="consistency">Consistency (ACID)</button>
<button class="tab" data-tab="schema">Schema &amp; Indexing</button>
<button class="tab" data-tab="cap">CAP Theorem</button>
<button class="tab" data-tab="views">Materialized Views</button>
<button class="tab" data-tab="vertical">Vertical Scaling</button>
<button class="tab active" data-tab="replication" data-tooltip="Write to primary, read from replica">Replication</button>
<button class="tab" data-tab="consistency" data-tooltip="ACID transactions with commit/rollback">Consistency (ACID)</button>
<button class="tab" data-tab="schema" data-tooltip="EXPLAIN query plans, add/drop indexes">Schema &amp; Indexing</button>
<button class="tab" data-tab="cap" data-tooltip="Simulate network partition, observe divergence">CAP Theorem</button>
<button class="tab" data-tab="views" data-tooltip="Compare JOINs vs pre-computed views">Materialized Views</button>
<button class="tab" data-tab="vertical" data-tooltip="Tune buffer pool size, benchmark queries">Vertical Scaling</button>
</div>
<button id="btn-reset-db" class="btn btn-reset" title="Reset database to initial state">Reset DB</button>
<button id="btn-reset-db" class="btn btn-reset" data-tooltip="Restore original 10 students, 4 courses, drop indexes" title="Reset database to initial state">Reset DB</button>
</header>

<div id="pattern-description" class="pattern-description">
Expand Down Expand Up @@ -161,7 +161,7 @@
<option>Engineering</option>
</select>
</div>
<button id="repl-write" class="btn btn-primary">Write &amp; Read</button>
<button id="repl-write" class="btn btn-primary" data-tooltip="INSERT on primary, then SELECT from replica to test replication">Write &amp; Read</button>
</div>

<!-- Consistency controls -->
Expand All @@ -184,7 +184,7 @@
<option>CS201</option><option>MATH101</option>
</select>
</div>
<button id="tx-transfer" class="btn btn-primary">Transfer Enrollment</button>
<button id="tx-transfer" class="btn btn-primary" data-tooltip="Move student between courses in an ACID transaction">Transfer Enrollment</button>
</div>

<!-- Schema controls -->
Expand All @@ -197,25 +197,25 @@
<label for="idx-resource">Resource</label>
<input type="text" id="idx-resource" value="resource-10" />
</div>
<button id="idx-explain" class="btn btn-primary">Run EXPLAIN</button>
<button id="idx-add" class="btn btn-secondary">Add Index</button>
<button id="idx-drop" class="btn btn-danger">Drop Index</button>
<button id="idx-explain" class="btn btn-primary" data-tooltip="Show MySQL query plan: rows scanned, index used">Run EXPLAIN</button>
<button id="idx-add" class="btn btn-secondary" data-tooltip="Create composite index on (student_id, resource)">Add Index</button>
<button id="idx-drop" class="btn btn-danger" data-tooltip="Remove the composite index">Drop Index</button>
</div>

<!-- CAP Theorem controls -->
<div id="controls-cap" class="controls">
<button id="cap-stop" class="btn btn-danger">Stop Replication</button>
<button id="cap-start" class="btn btn-secondary">Start Replication</button>
<button id="cap-test" class="btn btn-primary">Write &amp; Compare</button>
<button id="cap-stop" class="btn btn-danger" data-tooltip="STOP REPLICA -- simulate network partition">Stop Replication</button>
<button id="cap-start" class="btn btn-secondary" data-tooltip="START REPLICA -- recover from partition">Start Replication</button>
<button id="cap-test" class="btn btn-primary" data-tooltip="INSERT on primary, read from both, compare results">Write &amp; Compare</button>
</div>

<!-- Materialized Views controls -->
<div id="controls-views" class="controls">
<button id="views-create" class="btn btn-primary">Create View</button>
<button id="views-drop" class="btn btn-danger">Drop View</button>
<button id="views-join" class="btn btn-secondary">Query with JOINs</button>
<button id="views-view" class="btn btn-secondary">Query from View</button>
<button id="views-refresh" class="btn btn-secondary">Refresh View</button>
<button id="views-create" class="btn btn-primary" data-tooltip="CREATE TABLE ... AS SELECT (pre-compute the JOIN)">Create View</button>
<button id="views-drop" class="btn btn-danger" data-tooltip="DROP TABLE enrollment_summary">Drop View</button>
<button id="views-join" class="btn btn-secondary" data-tooltip="SELECT with 3-table JOIN (students+enrollments+courses)">Query with JOINs</button>
<button id="views-view" class="btn btn-secondary" data-tooltip="SELECT from the pre-computed flat table">Query from View</button>
<button id="views-refresh" class="btn btn-secondary" data-tooltip="Drop and recreate the view with latest data">Refresh View</button>
</div>

<!-- Vertical Scaling controls -->
Expand All @@ -229,8 +229,8 @@
<option value="268435456">256 MB</option>
</select>
</div>
<button id="vert-set" class="btn btn-secondary">Set Buffer</button>
<button id="vert-bench" class="btn btn-primary">Run Benchmark (200 queries)</button>
<button id="vert-set" class="btn btn-secondary" data-tooltip="SET GLOBAL innodb_buffer_pool_size">Set Buffer</button>
<button id="vert-bench" class="btn btn-primary" data-tooltip="Run 200 random queries and measure latency + hit ratio">Run Benchmark (200 queries)</button>
</div>
</section>

Expand Down Expand Up @@ -287,9 +287,10 @@ <h3>Indexes (access_log)</h3>
</div>

<footer class="bottom-bar">
<div class="resize-handle" id="resize-handle"></div>
<div class="bottom-split">
<div class="event-log-container">
<h3>Event Log</h3>
<h3>Event Log <span style="font-weight:400;font-size:0.7rem;color:var(--color-text-muted)">-- every database operation with timing</span></h3>
<div id="event-log" class="event-log"></div>
</div>

Expand All @@ -312,6 +313,7 @@ <h3>Event Log</h3>
</div>
</footer>

<div id="tooltip-popup" class="tooltip-popup"></div>
<script src="app.js"></script>
</body>
</html>
Binary file modified 10-databases/visualizer/screenshots/01-replication-tab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified 10-databases/visualizer/screenshots/02-sidebar-state.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified 10-databases/visualizer/screenshots/03-replication-result.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified 10-databases/visualizer/screenshots/05-schema-no-index.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified 10-databases/visualizer/screenshots/07-sql-console.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified 10-databases/visualizer/screenshots/08-cap-diverged.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified 10-databases/visualizer/screenshots/09-cap-consistent.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified 10-databases/visualizer/screenshots/10-views-join.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified 10-databases/visualizer/screenshots/11-views-view.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading