devloop-status.html 4.57 KB
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>DevLoop · Project Status</title>
<style>
  body { font-family: system-ui, -apple-system, sans-serif; max-width: 960px; margin: 24px auto; padding: 0 16px; color: #222; }
  header { display:flex; justify-content:space-between; align-items:baseline; border-bottom:1px solid #eee; padding-bottom:8px; margin-bottom:16px; }
  h1 { font-size: 18px; margin: 0; }
  .pid { color:#888; font-family:monospace; font-size:13px; }
  .row { display:flex; gap:16px; flex-wrap:wrap; margin-bottom:16px; }
  .card { background:#f7f7f9; border:1px solid #e3e3e8; border-radius:6px; padding:12px 16px; min-width:160px; }
  .card label { font-size:11px; text-transform:uppercase; color:#888; display:block; margin-bottom:4px; }
  .card .v { font-size:20px; font-weight:600; }
  .units { margin-top:8px; }
  .unit { display:flex; align-items:center; padding:6px 10px; background:#fff; border:1px solid #e3e3e8; border-radius:4px; margin-bottom:4px; gap:12px; }
  .unit .id { flex:1; font-family:monospace; }
  .unit .deps { color:#888; font-size:12px; }
  .status { display:inline-block; padding:2px 8px; border-radius:12px; font-size:11px; text-transform:uppercase; }
  .status.pending  { background:#eee; color:#666; }
  .status.coding   { background:#fff3cd; color:#856404; }
  .status.testing  { background:#cce5ff; color:#004085; }
  .status.passed   { background:#d4edda; color:#155724; }
  .status.failed   { background:#f8d7da; color:#721c24; }
  .status.blocked  { background:#d6d8db; color:#383d41; }
  .timeline { font-family:monospace; font-size:12px; max-height:240px; overflow:auto; background:#0f1115; color:#e6e6e6; padding:12px; border-radius:6px; }
  .timeline .ts { color:#7da; margin-right:8px; }
  .timeline .kind { color:#ffce6a; margin-right:8px; }
  input { font-family:monospace; padding:4px 8px; border:1px solid #ccc; border-radius:4px; }
  button { padding:4px 12px; background:#222; color:white; border:none; border-radius:4px; cursor:pointer; }
  .err { color:#c00; margin:8px 0; }
</style>
</head>
<body>
<header>
  <h1>DevLoop · Project Status</h1>
  <div>
    <input id="pidInput" placeholder="p-xxxxxx" />
    <button id="connectBtn">Connect</button>
  </div>
</header>

<div id="err" class="err"></div>

<div id="content" style="display:none">
  <div class="pid">pid: <span id="pidShow"></span> · root: <span id="rootShow"></span></div>
  <div class="row">
    <div class="card"><label>PRD rev</label><div class="v" id="prdRev">-</div></div>
    <div class="card"><label>SDD rev</label><div class="v" id="sddRev">-</div></div>
    <div class="card"><label>Units</label><div class="v" id="unitsCount">0</div></div>
  </div>
  <h2 style="font-size:14px;margin:16px 0 8px">Units (DAG)</h2>
  <div class="units" id="units"></div>
  <h2 style="font-size:14px;margin:16px 0 8px">Recent timeline events</h2>
  <div class="timeline" id="timeline"></div>
</div>

<script>
const $ = id => document.getElementById(id);
let pollTimer = null;

async function fetchState(pid) {
  const r = await fetch(`/api/devloop/projects/${pid}/state`);
  if (!r.ok) throw new Error(`state: ${r.status}`);
  return r.json();
}

function render(s) {
  $('pidShow').textContent = s.pid;
  $('rootShow').textContent = s.root_path || '';
  $('prdRev').textContent = s.prd_rev ?? '-';
  $('sddRev').textContent = s.sdd_rev ?? '-';
  const units = s.units || [];
  $('unitsCount').textContent = units.length;
  $('units').innerHTML = units.map(u => `
    <div class="unit">
      <span class="id">${u.id}</span>
      <span class="deps">deps: ${(u.depends_on || []).join(', ') || '∅'}</span>
      <span class="status ${u.status}">${u.status}</span>
    </div>`).join('');
  $('timeline').innerHTML = (s.timeline_tail || []).map(e => `
    <div><span class="ts">${e.ts || ''}</span><span class="kind">[${e.kind}]</span>${e.summary || ''}</div>
  `).join('');
}

async function connect() {
  const pid = $('pidInput').value.trim();
  if (!pid) return;
  $('err').textContent = '';
  if (pollTimer) clearInterval(pollTimer);
  async function poll() {
    try {
      const s = await fetchState(pid);
      render(s);
      $('content').style.display = '';
    } catch (e) {
      $('err').textContent = e.message;
      $('content').style.display = 'none';
    }
  }
  await poll();
  pollTimer = setInterval(poll, 2000);
}

$('connectBtn').addEventListener('click', connect);
$('pidInput').addEventListener('keydown', e => { if (e.key === 'Enter') connect(); });
const fromQuery = new URLSearchParams(location.search).get('pid');
if (fromQuery) { $('pidInput').value = fromQuery; connect(); }
</script>
</body>
</html>