245 lines
7.7 KiB
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>
|