fester/ui/debugger.html

245 lines
7.7 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fester — Debugger</title>
<link rel="stylesheet" href="/style.css">
<style>
body { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
#wrap { flex: 1; display: grid; grid-template-columns: 280px 1fr 340px; gap: 12px; padding: 12px; overflow: hidden; }
#wrap > section { display: flex; flex-direction: column; min-height: 0; }
#wrap > section > .body { flex: 1; overflow: auto; min-height: 0; }
.controls-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
.preview {
font-family: var(--font-mono);
font-size: 11px;
background: var(--bg-0);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 8px;
max-height: 200px;
overflow: auto;
}
.tl-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-bottom: 1px solid var(--line);
font-family: var(--font-mono);
font-size: 11px;
}
.tl-row:last-child { border-bottom: 0; }
.tl-row .dot {
width: 10px; height: 10px;
border-radius: 50%;
background: var(--fg-3);
flex-shrink: 0;
}
.tl-row.done .dot { background: var(--ok); }
.tl-row.running .dot { background: var(--warn); animation: pulse 1.2s infinite; }
.tl-row.failed .dot { background: var(--err); }
.tl-row.current { background: var(--bg-4); border-left: 3px solid var(--accent); padding-left: 5px; }
.tl-row .name { flex: 1; color: var(--fg-0); }
.tl-row .node { color: var(--fg-2); }
@media (max-width: 1100px) {
#wrap { grid-template-columns: 240px 1fr; }
#wrap > section.right { display: none; }
}
</style>
</head>
<body data-fester-shell="debugger">
<div id="wrap">
<!-- LEFT: controls -->
<section class="panel">
<div class="head">
<h3>Debugger</h3>
<span class="badge" id="state-badge">idle</span>
</div>
<div class="body">
<div class="controls-grid">
<button id="btn-pause" class="danger">⏸ Pause</button>
<button id="btn-resume" class="primary">▶ Resume</button>
<button id="btn-step">⏭ Step</button>
<button id="btn-step-back">⏮ Back</button>
</div>
<hr>
<h3>State</h3>
<pre class="json" id="state"></pre>
<hr>
<h3>Next Action Preview</h3>
<div class="preview" id="preview"></div>
</div>
</section>
<!-- CENTER: timeline -->
<section class="panel">
<div class="head">
<h3>Execution Timeline</h3>
<span class="dim" id="step-label">step 0</span>
</div>
<div class="body" style="padding:0;">
<div id="timeline"></div>
</div>
</section>
<!-- RIGHT: inspector -->
<section class="panel right">
<div class="head"><h3>Inspector</h3></div>
<div class="body">
<pre class="json" id="inspector">Click a timeline row to inspect.</pre>
</div>
</section>
</div>
<script src="/ui/app.js"></script>
<script>
(function () {
'use strict';
const $ = (id) => document.getElementById(id);
let wsDebug = null;
let currentState = { paused: false, current_step: 0, engine_attached: false };
let timeline = [];
function setStateBadge() {
const b = $('state-badge');
if (!currentState.engine_attached) {
b.textContent = 'no engine';
b.className = 'badge';
return;
}
if (currentState.paused) {
b.textContent = 'paused';
b.className = 'badge warn';
} else {
b.textContent = 'running';
b.className = 'badge ok';
}
}
async function fetchState() {
try {
const s = await Fester.api('/debugger/state');
currentState = s;
$('state').textContent = JSON.stringify(s, null, 2);
$('step-label').textContent = 'step ' + s.current_step;
setStateBadge();
} catch (e) {
$('state').textContent = e.message;
}
}
async function postAction(action) {
try {
await Fester.api(`/debugger/${action}`, { method: 'POST' });
Fester.toast(`${action} sent`, 'ok');
fetchState();
} catch (e) {
Fester.toast(`${action} failed: ${e.message}`, 'err');
}
}
function connectWs() {
try {
wsDebug = new WebSocket(Fester.wsDebuggerUrl);
wsDebug.onmessage = (msg) => {
try {
const event = JSON.parse(msg.data);
if (event.type === 'timeline-update') {
renderTimeline(event.data.events || []);
if (event.data.current) {
$('preview').textContent = JSON.stringify(event.data.current, null, 2);
}
}
} catch (e) {
console.error('bad ws-debugger payload', e);
}
};
wsDebug.onclose = () => {
setTimeout(connectWs, 2000);
};
} catch (e) {
console.warn('ws-debugger unavailable', e);
}
}
function renderTimeline(events) {
timeline = events;
const el = $('timeline');
if (!events.length) {
el.innerHTML = '<div class="dim center" style="padding:20px;">No timeline events</div>';
return;
}
el.innerHTML = events.map((ev, i) => {
const cls = ev.completed ? 'done' : 'running';
const isCurrent = i === events.length - 1 && !ev.completed;
return `
<div class="tl-row ${cls} ${isCurrent ? 'current' : ''}" data-idx="${i}">
<span class="dot"></span>
<span class="name">${ev.name}</span>
<span class="node">${ev.node || ''}</span>
</div>
`;
}).join('');
// attach click handlers
el.querySelectorAll('.tl-row').forEach(row => {
row.addEventListener('click', () => {
const idx = parseInt(row.dataset.idx);
$('inspector').textContent = JSON.stringify(events[idx], null, 2);
});
});
}
// Buttons
$('btn-pause').addEventListener('click', () => postAction('pause'));
$('btn-resume').addEventListener('click', () => postAction('resume'));
$('btn-step').addEventListener('click', () => postAction('step'));
$('btn-step-back').addEventListener('click', () => postAction('step-back'));
// Subscribe to live events as well (for the timeline)
Fester.subscribe(event => {
if (event.type === 'pipeline_update' || event.type === 'task_update' || event.type === 'pipeline') {
const d = event.data || event;
// upsert into timeline
const existing = timeline.find(t => t.name === d.action);
if (existing) {
existing.completed = d.state === 'done';
existing.node = d.node;
} else {
timeline.push({
name: d.action,
completed: d.state === 'done',
node: d.node,
state: d.state,
});
}
renderTimeline(timeline);
}
});
// Init
fetchState();
setInterval(fetchState, 3000);
connectWs();
})();
</script>
</body>
</html>