fester/ui/replay.html

284 lines
5.4 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Fester Execution Replay Observatory</title>
<style>
body {
margin: 0;
font-family: sans-serif;
background: #0b0f14;
color: #d0d0d0;
}
#layout {
display: grid;
grid-template-columns: 280px 1fr 320px;
height: 100vh;
}
/* -------------------------
LEFT: CONTROLS
------------------------- */
#controls {
padding: 12px;
border-right: 1px solid #1f2a38;
background: #0e141d;
}
button {
width: 100%;
margin: 4px 0;
padding: 6px;
background: #1a2432;
color: #ddd;
border: 1px solid #2c3b4d;
}
/* timeline slider */
#timeline {
width: 100%;
}
/* -------------------------
CENTER: GRAPH
------------------------- */
#graph {
position: relative;
background: #05070b;
overflow: hidden;
}
/* -------------------------
RIGHT: DEBUG PANEL
------------------------- */
#debug {
padding: 12px;
border-left: 1px solid #1f2a38;
background: #0e141d;
font-size: 12px;
overflow-y: auto;
}
/* NODE */
.node {
position: absolute;
width: 120px;
padding: 6px;
font-size: 11px;
text-align: center;
border-radius: 6px;
background: #1b2430;
border: 1px solid #333;
transition: all 0.2s ease;
}
/* STATES */
.node.pending { border-color: #455a64; }
.node.running { border-color: #ffb300; box-shadow: 0 0 10px rgba(255,179,0,0.3); }
.node.done { border-color: #66bb6a; }
.node.failed { border-color: #ef5350; }
.node.cache { border-color: #42a5f5; }
/* highlight */
.node.active {
outline: 2px solid #ffd54f;
}
</style>
</head>
<body>
<div id="layout">
<!-- LEFT -->
<div id="controls">
<h3>Replay Controls</h3>
<button onclick="loadReplay()">Load Replay</button>
<button onclick="play()">▶ Play</button>
<button onclick="pause()">⏸ Pause</button>
<button onclick="step()">⏭ Step</button>
<button onclick="back()">⏮ Back</button>
<hr>
<input id="timeline" type="range" min="0" max="1" value="0" step="1"
oninput="seek(this.value)">
<div id="timeLabel">0</div>
</div>
<!-- CENTER GRAPH -->
<div id="graph"></div>
<!-- RIGHT DEBUG -->
<div id="debug">
<h3>Inspector</h3>
<pre id="inspector">Click a node</pre>
</div>
<div id="autopsyPanel">
<h3>Failure Autopsy</h3>
<input id="failAction" placeholder="action name"/>
<button onclick="runAutopsy()">Analyze</button>
<pre id="autopsyOut"></pre>
</div>
</div>
<script>
let sessionId = null;
let events = [];
let index = 0;
let timer = null;
const nodes = {};
// -------------------------
// LOAD REPLAY
// -------------------------
async function loadReplay() {
const res = await fetch("/replay/start", {
method: "POST"
});
const data = await res.json();
sessionId = data.session_id;
// in real system: fetch full journal snapshot
const journal = await fetch("/replay/events")
.then(r => r.json())
.catch(() => ({ events: [] }));
events = journal.events || [];
document.getElementById("timeline").max = events.length - 1;
}
async function runAutopsy() {
const action = document.getElementById("failAction").value;
const res = await fetch(`/autopsy/${sessionId}/${action}`);
const data = await res.json();
document.getElementById("autopsyOut").innerText =
JSON.stringify(data, null, 2);
}
// -------------------------
// RENDER EVENT
// -------------------------
function applyEvent(e) {
if (!e || !e.data) return;
const d = e.data;
const name = d.node || d.action;
if (!nodes[name]) {
const el = document.createElement("div");
el.className = "node";
el.innerText = name;
// deterministic layout
const x = 80 + (Object.keys(nodes).length % 5) * 160;
const y = 80 + Math.floor(Object.keys(nodes).length / 5) * 120;
el.style.left = x + "px";
el.style.top = y + "px";
el.onclick = () => inspect(d);
nodes[name] = el;
document.getElementById("graph").appendChild(el);
}
const el = nodes[name];
el.classList.remove("pending","running","done","failed","cache");
if (d.state) el.classList.add(d.state);
if (d.cache === "hit") el.classList.add("cache");
}
// -------------------------
// INSPECTOR
// -------------------------
function inspect(data) {
document.getElementById("inspector").innerText =
JSON.stringify(data, null, 2);
}
// -------------------------
// PLAYBACK ENGINE
// -------------------------
function renderFrame(i) {
index = i;
document.getElementById("timeline").value = i;
document.getElementById("timeLabel").innerText = i;
// reset view
document.getElementById("graph").innerHTML = "";
Object.keys(nodes).forEach(k => delete nodes[k]);
// replay up to index
for (let j = 0; j <= i; j++) {
applyEvent(events[j]);
}
}
// -------------------------
// CONTROLS
// -------------------------
function play() {
pause();
timer = setInterval(() => {
if (index < events.length - 1) {
renderFrame(++index);
} else {
pause();
}
}, 300);
}
function pause() {
clearInterval(timer);
}
function step() {
if (index < events.length - 1) {
renderFrame(++index);
}
}
function back() {
if (index > 0) {
renderFrame(--index);
}
}
function seek(i) {
renderFrame(parseInt(i));
}
</script>
</body>
</html>