← back to Yolo Agent

live-dashboard.html

217 lines

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>YOLO Live Tasks</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,system-ui,sans-serif;background:#0a0a0a;color:#e0e0e0;padding:16px}
h1{font-size:22px;color:#f97316;margin-bottom:4px}
.sub{color:#666;font-size:12px;margin-bottom:16px}
.live-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:#22c55e;margin-right:6px;animation:pulse 1.5s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.3}}
.stats{display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap}
.stat{background:#1a1a1a;border:1px solid #333;border-radius:6px;padding:10px 16px;min-width:100px}
.stat .n{font-size:24px;font-weight:700;color:#f97316}
.stat .l{font-size:10px;color:#888;text-transform:uppercase;letter-spacing:.05em}
.task-row{background:#1a1a1a;border:1px solid #333;border-radius:6px;margin-bottom:4px;overflow:hidden;transition:border-color 0.2s}
.task-row.running{border-color:#f97316}
.task-row.failed{border-color:#ef4444}
.task-header{display:flex;align-items:center;padding:8px 12px;cursor:pointer;gap:8px;font-size:13px}
.task-header:hover{background:#222}
.task-chevron{color:#555;font-size:10px;transition:transform 0.2s;width:12px}
.task-row.open .task-chevron{transform:rotate(90deg)}
.task-status{padding:2px 8px;border-radius:4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}
.s-running{background:#7c2d12;color:#fdba74}
.s-done{background:#14532d;color:#86efac}
.s-failed{background:#7f1d1d;color:#fca5a5}
.s-queued{background:#1e3a5f;color:#93c5fd}
.task-name{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.task-time{color:#666;font-size:11px;flex-shrink:0;font-family:monospace}
.task-duration{color:#888;font-size:11px;flex-shrink:0}
.task-detail{display:none;padding:8px 12px 12px 32px;font-size:12px;color:#999;border-top:1px solid #222;max-height:300px;overflow-y:auto}
.task-row.open .task-detail{display:block}
.task-detail pre{background:#111;padding:8px;border-radius:4px;font-size:11px;white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;margin-top:6px}
.section-label{font-size:11px;color:#555;text-transform:uppercase;letter-spacing:.08em;margin:12px 0 6px;font-weight:600}
</style></head><body>
<h1><span class="live-dot"></span>YOLO Live Tasks</h1>
<p class="sub">Auto-refreshes every 3s | Port 9670</p>
<div class="stats" id="stats"></div>
<div class="section-label">CURRENT</div>
<div id="current"></div>
<div class="section-label">QUEUE</div>
<div id="queue"></div>
<div class="section-label" id="failed-label" style="display:none;color:#ef4444;">FAILED — Awaiting Retry <button id="retry-btn" style="margin-left:12px;padding:3px 12px;border:1px solid #ef4444;border-radius:4px;background:rgba(239,68,68,.1);color:#fca5a5;font-size:10px;font-weight:700;cursor:pointer;text-transform:uppercase;">Retry All Now</button></div>
<div id="failed"></div>
<div class="section-label">COMPLETED (last 30)</div>
<div id="history"></div>

<script>
var openTasks = {};


function toggle(id) {
  openTasks[id] = !openTasks[id];
  var el = document.getElementById('row-' + id);
  if (el) el.classList.toggle('open');
}

function dur(ms) {
  if (!ms) return '';
  var s = Math.floor(ms / 1000);
  if (s < 60) return s + 's';
  return Math.floor(s / 60) + 'm ' + (s % 60) + 's';
}

function durStr(str) {
  if (!str) return '';
  return str.replace('s', 's');
}

function taskRow(name, status, id, opts) {
  opts = opts || {};
  var sc = status === 'running' ? 's-running' : status === 'done' ? 's-done' : status === 'failed' ? 's-failed' : 's-queued';
  var displayName = name.replace('.md', '');
  var time = opts.time || '';
  var d = opts.duration || '';
  var detail = opts.detail || '';

  var el = document.createElement('div');
  el.className = 'task-row ' + status + (openTasks[id] ? ' open' : '');
  el.id = 'row-' + id;

  var header = document.createElement('div');
  header.className = 'task-header';
  header.onclick = function() { toggle(id); };

  var chev = document.createElement('span'); chev.className = 'task-chevron'; chev.textContent = '\u25B6';
  var tidBadge = document.createElement('span'); tidBadge.className = 'task-id'; tidBadge.style.cssText = 'font-family:JetBrains Mono,monospace;font-size:.7em;color:#60a5fa;font-weight:700;margin-right:6px;min-width:28px;';
  tidBadge.textContent = opts.taskId ? '#' + opts.taskId : '';
  var badge = document.createElement('span'); badge.className = 'task-status ' + sc; badge.textContent = status;
  var nm = document.createElement('span'); nm.className = 'task-name'; nm.textContent = displayName;
  var du = document.createElement('span'); du.className = 'task-duration'; du.textContent = d;
  var tm = document.createElement('span'); tm.className = 'task-time'; tm.textContent = time;

  header.appendChild(chev); header.appendChild(tidBadge); header.appendChild(badge); header.appendChild(nm); header.appendChild(du); header.appendChild(tm);
  el.appendChild(header);

  var det = document.createElement('div'); det.className = 'task-detail';
  if (detail) {
    var pre = document.createElement('pre');
    pre.textContent = detail.substring(0, 2000);
    det.appendChild(pre);
  } else {
    var em = document.createElement('em'); em.textContent = 'Click to expand when output available';
    det.appendChild(em);
  }
  el.appendChild(det);
  return el;
}

function addStat(parent, num, label, color) {
  var d = document.createElement('div'); d.className = 'stat';
  var n = document.createElement('div'); n.className = 'n'; n.textContent = num;
  if (color) n.style.color = color;
  var l = document.createElement('div'); l.className = 'l'; l.textContent = label;
  d.appendChild(n); d.appendChild(l); parent.appendChild(d);
}

function refresh() {
  
  Promise.all([
    fetch('/live-api/status').then(function(r) { return r.json(); }).catch(function() { return {}; }),
    fetch('/live-api/queue').then(function(r) { return r.json(); }).catch(function() { return []; }),
    fetch('/live-api/history').then(function(r) { return r.json(); }).catch(function() { return []; }),
    fetch('/live-api/failed').then(function(r) { return r.json(); }).catch(function() { return []; })
  ]).then(function(res) {
    var status = res[0];
    var queue = Array.isArray(res[1]) ? res[1] : [];
    var history = Array.isArray(res[2]) ? res[2] : [];
    var failed = Array.isArray(res[3]) ? res[3] : [];

    var isRunning = status.running || false;
    var currentTask = status.currentTask || null;
    var startedAt = status.startedAt || null;
    var statusText = isRunning ? (status.paused ? 'PAUSED' : 'RUNNING') : 'IDLE';
    var statusColor = isRunning ? (status.paused ? '#f59e0b' : '#22c55e') : '#666';

    // Stats
    var statsEl = document.getElementById('stats');
    statsEl.textContent = '';
    addStat(statsEl, currentTask ? '1' : '0', 'Running');
    addStat(statsEl, String(queue.length), 'Queued');
    addStat(statsEl, String(failed.length), 'Failed', failed.length > 0 ? '#ef4444' : '#22c55e');
    addStat(statsEl, String(history.length), 'Done');
    addStat(statsEl, statusText, 'Status', statusColor);

    // Current
    var curEl = document.getElementById('current');
    curEl.textContent = '';
    var currentTaskId = status.currentTaskId || 0;
    if (currentTask) {
      var elapsed = startedAt ? dur(Date.now() - new Date(startedAt).getTime()) : '';
      var time = startedAt ? new Date(startedAt).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }) : '';
      curEl.appendChild(taskRow(currentTask, 'running', 'current', { duration: elapsed, time: time, taskId: currentTaskId }));
    } else {
      var p = document.createElement('div'); p.style.cssText = 'color:#555;font-size:12px;padding:8px'; p.textContent = 'No task running';
      curEl.appendChild(p);
    }

    // Queue
    var qEl = document.getElementById('queue');
    qEl.textContent = '';
    if (queue.length === 0) {
      var p = document.createElement('div'); p.style.cssText = 'color:#555;font-size:12px;padding:8px'; p.textContent = 'Queue empty';
      qEl.appendChild(p);
    } else {
      queue.forEach(function(t, i) {
        var name = t.name || t;
        var time = t.created ? new Date(t.created).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }) : '';
        var queueId = currentTaskId + i + 1;
        qEl.appendChild(taskRow(name, 'queued', 'q' + i, { time: time, taskId: queueId }));
      });
    }

    // Failed tasks
    var fLabel = document.getElementById('failed-label');
    var fEl = document.getElementById('failed');
    fEl.textContent = '';
    if (failed.length > 0) {
      fLabel.style.display = 'block';
      failed.forEach(function(f, i) {
        fEl.appendChild(taskRow(f.name, 'failed', 'f' + i, { taskId: '!' }));
      });
    } else {
      fLabel.style.display = 'none';
    }

    // History (last 30, newest first)
    var hEl = document.getElementById('history');
    hEl.textContent = '';
    if (history.length === 0) {
      var p = document.createElement('div'); p.style.cssText = 'color:#555;font-size:12px;padding:8px'; p.textContent = 'No history';
      hEl.appendChild(p);
    } else {
      history.slice(-30).reverse().forEach(function(h, i) {
        var st = h.exitCode === 0 ? 'done' : 'failed';
        var name = h.task || 'unknown';
        var time = h.completedAt ? new Date(h.completedAt).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }) : '';
        var d = h.duration || '';
        var output = (h.stdout || '').substring(0, 1500);
        if (h.stderr) output += '\n--- STDERR ---\n' + h.stderr.substring(0, 500);
        hEl.appendChild(taskRow(name, st, 'h' + i, { duration: d, time: time, detail: output, taskId: h.taskId }));
      });
    }
  });
}

document.getElementById('retry-btn').addEventListener('click', function() {
  fetch('/api/retry-failed', { method: 'POST' })
    .then(function(r) { return r.json(); })
    .then(function(d) { refresh(); })
    .catch(function() {});
});

refresh();
setInterval(refresh, 3000);
</script>
</body></html>