devloop-status.html
4.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
<!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>