devloop-status.html 7.98 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; }
  .ok-flash { color:#080; margin:8px 0; }
  .status-badge { font-size:11px; padding:3px 10px; border-radius:12px; margin-left:10px; text-transform:uppercase; }
  .status-badge.draft         { background:#eee; color:#666; }
  .status-badge.running       { background:#cce5ff; color:#004085; }
  .status-badge.shipped       { background:#d4edda; color:#155724; }
  .status-badge.shipped-final { background:#0a6;   color:#fff; }
  .status-badge.abandoned     { background:#222;   color:#fff; }
  .status-badge.l2_blocked    { background:#fff3cd; color:#856404; }
  .status-badge.l2_exhausted  { background:#f8d7da; color:#721c24; }
  .controls { display:flex; gap:8px; margin:16px 0; flex-wrap:wrap; }
  .controls button { padding:6px 14px; }
  .controls button.signoff { background:#0a6; }
  .controls button.abandon { background:#822; }
  .controls button:disabled { background:#ccc; cursor:not-allowed; }
  textarea { width:100%; min-height:60px; padding:8px; border:1px solid #ccc; border-radius:4px; font-family:inherit; box-sizing:border-box; }
</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><span id="statusBadge" class="status-badge"></span> · root: <span id="rootShow"></span></div>

  <div id="opMsg" class="ok-flash"></div>

  <div class="controls">
    <button id="signoffBtn" class="signoff" title="只有 status=shipped 时可以">Sign Off</button>
    <button id="abandonBtn" class="abandon">Abandon</button>
  </div>

  <h2 style="font-size:14px;margin:16px 0 8px">Submit feedback (PM incremental)</h2>
  <textarea id="feedbackText" placeholder="新功能 / 修订需求,例如: 增加 toggle done 切换"></textarea>
  <div style="margin-top:8px"><button id="feedbackBtn">Submit feedback</button></div>

  <div class="row" style="margin-top:16px">
    <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 || '';
  const status = s.status || s.project?.status || 'unknown';
  const sb = $('statusBadge');
  sb.textContent = status;
  sb.className = 'status-badge ' + status;
  $('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('');

  // 控制按钮可点性
  $('signoffBtn').disabled = status !== 'shipped';
  $('abandonBtn').disabled = status === 'shipped-final' || status === 'abandoned';
  $('feedbackBtn').disabled = status === 'shipped-final' || status === 'abandoned';
}

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(); });

async function postAction(endpoint, body) {
  const pid = $('pidInput').value.trim();
  if (!pid) return;
  $('opMsg').textContent = '...';
  try {
    const r = await fetch(`/api/devloop/projects/${pid}/${endpoint}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body || {}),
    });
    const j = await r.json();
    if (!r.ok) {
      $('opMsg').textContent = `error: ${j.error || r.status}`;
      $('opMsg').className = 'err';
    } else {
      $('opMsg').textContent = `${endpoint} ok · status=${j.status || j.pid || ''}`;
      $('opMsg').className = 'ok-flash';
    }
  } catch (e) {
    $('opMsg').textContent = 'network error: ' + e.message;
    $('opMsg').className = 'err';
  }
  // poll once immediately to refresh
  const s = await fetchState($('pidInput').value.trim()); render(s);
}

$('signoffBtn').addEventListener('click', () => postAction('sign-off', { by: 'human' }));
$('abandonBtn').addEventListener('click', () => {
  if (!confirm('Abandon this project? (irreversible)')) return;
  postAction('abandon', { by: 'human', reason: 'user clicked abandon' });
});
$('feedbackBtn').addEventListener('click', () => {
  const text = $('feedbackText').value.trim();
  if (!text) { $('opMsg').textContent = 'feedback text required'; $('opMsg').className='err'; return; }
  postAction('feedback', { text, by: 'human' });
  $('feedbackText').value = '';
});

const fromQuery = new URLSearchParams(location.search).get('pid');
if (fromQuery) { $('pidInput').value = fromQuery; connect(); }
</script>
</body>
</html>