@atolis-hq/wake 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +140 -140
- package/dist/src/adapters/http/ui-assets.js +300 -300
- package/dist/src/version.js +1 -1
- package/docker/Dockerfile +54 -54
- package/docker/setup.sh +0 -0
- package/package.json +56 -52
- package/prompts/implement.md +49 -49
- package/prompts/refine.md +44 -44
- package/dist/src/adapters/claude/prompt-templates.js +0 -57
- package/dist/src/cli/locks-command.js +0 -19
|
@@ -1,302 +1,302 @@
|
|
|
1
1
|
import { wakeVersion } from '../../version.js';
|
|
2
|
-
export const indexHtml = `<!DOCTYPE html>
|
|
3
|
-
<html lang="en">
|
|
4
|
-
<head>
|
|
5
|
-
<meta charset="utf-8" />
|
|
6
|
-
<title>Wake control plane</title>
|
|
7
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
-
<style>
|
|
9
|
-
:root { color-scheme: dark light; }
|
|
10
|
-
body { font-family: -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; background: #14161a; color: #e8e8e8; }
|
|
11
|
-
header { display: flex; align-items: center; gap: 1rem; padding: 0.6rem 1rem; background: #1d2026; border-bottom: 1px solid #2c313a; flex-wrap: wrap; }
|
|
12
|
-
header h1 { font-size: 1rem; margin: 0; font-weight: 600; }
|
|
13
|
-
.version { color: #9aa2ad; font-size: 0.78rem; }
|
|
14
|
-
.pill { padding: 0.15rem 0.5rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
|
|
15
|
-
.pill-idle { background: #1f3d2c; color: #7fe3a3; }
|
|
16
|
-
.pill-ticking { background: #1f3350; color: #7fb3ff; }
|
|
17
|
-
.pill-paused { background: #4a3510; color: #ffcf7f; }
|
|
18
|
-
nav { display: flex; gap: 0.25rem; padding: 0.5rem 1rem; background: #191c22; border-bottom: 1px solid #2c313a; }
|
|
19
|
-
nav button { background: none; border: none; color: #9aa2ad; padding: 0.4rem 0.8rem; cursor: pointer; border-radius: 6px; font-size: 0.85rem; }
|
|
20
|
-
nav button.active { background: #262b33; color: #fff; }
|
|
21
|
-
main { padding: 1rem; }
|
|
22
|
-
.columns { display: grid; grid-template-columns: repeat(6, minmax(180px, 1fr)); gap: 0.6rem; overflow-x: auto; }
|
|
23
|
-
.col { background: #1a1d23; border-radius: 8px; padding: 0.5rem; min-height: 200px; }
|
|
24
|
-
.col h2 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: #9aa2ad; margin: 0.2rem 0.4rem 0.5rem; }
|
|
25
|
-
.card { background: #22262e; border: 1px solid #2c313a; border-radius: 6px; padding: 0.5rem; margin-bottom: 0.5rem; cursor: pointer; font-size: 0.8rem; }
|
|
26
|
-
.card:hover { border-color: #4d5866; }
|
|
27
|
-
.card .title { font-weight: 600; margin-bottom: 0.25rem; }
|
|
28
|
-
.card .meta { color: #9aa2ad; font-size: 0.72rem; }
|
|
29
|
-
.chip { display: inline-block; background: #2c313a; border-radius: 4px; padding: 0.05rem 0.35rem; font-size: 0.68rem; margin-right: 0.2rem; }
|
|
30
|
-
table { border-collapse: collapse; width: 100%; font-size: 0.8rem; }
|
|
31
|
-
th, td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid #2c313a; }
|
|
32
|
-
th { color: #9aa2ad; font-weight: 600; }
|
|
33
|
-
pre { background: #1a1d23; padding: 0.75rem; border-radius: 6px; overflow: auto; font-size: 0.75rem; }
|
|
34
|
-
.drawer { position: fixed; top: 0; right: 0; width: min(560px, 100%); height: 100%; background: #191c22; border-left: 1px solid #2c313a; overflow-y: auto; padding: 1rem; transform: translateX(100%); transition: transform 0.15s ease; }
|
|
35
|
-
.drawer.open { transform: translateX(0); }
|
|
36
|
-
.drawer .close { float: right; cursor: pointer; color: #9aa2ad; }
|
|
37
|
-
.tiles { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
|
38
|
-
.tile { background: #1a1d23; border-radius: 8px; padding: 0.6rem 0.9rem; min-width: 120px; }
|
|
39
|
-
.tile .n { font-size: 1.3rem; font-weight: 700; }
|
|
40
|
-
.tile .l { color: #9aa2ad; font-size: 0.72rem; text-transform: uppercase; }
|
|
41
|
-
.amber { color: #ffcf7f; }
|
|
42
|
-
.red { color: #ff8f7f; }
|
|
43
|
-
.ok { color: #7fe3a3; }
|
|
44
|
-
input[type=text] { background: #1a1d23; border: 1px solid #2c313a; color: #e8e8e8; padding: 0.3rem 0.5rem; border-radius: 6px; margin-bottom: 0.6rem; width: 260px; }
|
|
45
|
-
</style>
|
|
46
|
-
</head>
|
|
47
|
-
<body>
|
|
48
|
-
<header>
|
|
49
|
-
<h1>Wake control plane</h1>
|
|
50
|
-
<span class="version">${wakeVersion}</span>
|
|
51
|
-
<span id="loop-pill" class="pill">…</span>
|
|
52
|
-
<span id="status-summary" class="meta"></span>
|
|
53
|
-
</header>
|
|
54
|
-
<nav>
|
|
55
|
-
<button data-view="board" class="active">Board</button>
|
|
56
|
-
<button data-view="activity">Activity</button>
|
|
57
|
-
<button data-view="runs">Runs</button>
|
|
58
|
-
<button data-view="config">Config</button>
|
|
59
|
-
<button data-view="health">Health</button>
|
|
60
|
-
</nav>
|
|
61
|
-
<main id="main"></main>
|
|
62
|
-
<div id="drawer" class="drawer"><span class="close" id="drawer-close">close ✕</span><div id="drawer-body"></div></div>
|
|
63
|
-
<script>
|
|
64
|
-
const API = '/api/v1';
|
|
65
|
-
const CONDITIONS = ['needs-human', 'active', 'ready', 'waiting', 'stalled', 'finished'];
|
|
66
|
-
let currentView = 'board';
|
|
67
|
-
|
|
68
|
-
async function getJson(path) {
|
|
69
|
-
const res = await fetch(API + path);
|
|
70
|
-
if (!res.ok) throw new Error(path + ' -> ' + res.status);
|
|
71
|
-
return res.json();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function fmtMs(ms) {
|
|
75
|
-
if (ms === undefined || ms === null) return '—';
|
|
76
|
-
const s = Math.floor(ms / 1000);
|
|
77
|
-
if (s < 60) return s + 's';
|
|
78
|
-
const m = Math.floor(s / 60);
|
|
79
|
-
if (m < 60) return m + 'm';
|
|
80
|
-
const h = Math.floor(m / 60);
|
|
81
|
-
if (h < 24) return h + 'h';
|
|
82
|
-
return Math.floor(h / 24) + 'd';
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function el(tag, attrs, children) {
|
|
86
|
-
const node = document.createElement(tag);
|
|
87
|
-
for (const [k, v] of Object.entries(attrs || {})) {
|
|
88
|
-
if (k === 'text') node.textContent = v;
|
|
89
|
-
else if (k.startsWith('on')) node.addEventListener(k.slice(2), v);
|
|
90
|
-
else node.setAttribute(k, v);
|
|
91
|
-
}
|
|
92
|
-
for (const child of children || []) node.appendChild(child);
|
|
93
|
-
return node;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function renderStatusBar() {
|
|
97
|
-
try {
|
|
98
|
-
const status = await getJson('/status');
|
|
99
|
-
const pill = document.getElementById('loop-pill');
|
|
100
|
-
pill.textContent = status.loopState;
|
|
101
|
-
pill.className = 'pill pill-' + status.loopState;
|
|
102
|
-
const freshness = status.sourceFreshness.level;
|
|
103
|
-
const summary = document.getElementById('status-summary');
|
|
104
|
-
summary.textContent =
|
|
105
|
-
'runs today: ' + status.runsToday + ' · failures today: ' + status.failuresToday +
|
|
106
|
-
' · cost today: $' + Number(status.costUsdToday ?? 0).toFixed(2) +
|
|
107
|
-
' · source freshness: ' + freshness +
|
|
108
|
-
(status.lastRun ? ' · last run: ' + status.lastRun.repo + '#' + status.lastRun.issueNumber + ' ' + status.lastRun.action + ' → ' + (status.lastRun.sentinel ?? status.lastRun.status) : '');
|
|
109
|
-
} catch (err) {
|
|
110
|
-
document.getElementById('status-summary').textContent = 'status unavailable: ' + err.message;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function renderBoard() {
|
|
115
|
-
const board = await getJson('/board');
|
|
116
|
-
const main = document.getElementById('main');
|
|
117
|
-
main.innerHTML = '';
|
|
118
|
-
const columns = el('div', { class: 'columns' }, CONDITIONS.map((cond) => {
|
|
119
|
-
const items = board.filter((c) => c.condition === cond);
|
|
120
|
-
const cards = items.map((item) => el('div', {
|
|
121
|
-
class: 'card',
|
|
122
|
-
onclick: () => openItem(item.repo, item.number),
|
|
123
|
-
}, [
|
|
124
|
-
el('div', { class: 'title', text: item.repo + '#' + item.number + ' ' + item.title }),
|
|
125
|
-
el('div', { class: 'meta' }, [
|
|
126
|
-
el('span', { class: 'chip', text: item.stage }),
|
|
127
|
-
document.createTextNode(fmtMs(item.timeInStageMs) + ' in stage'),
|
|
128
|
-
]),
|
|
129
|
-
el('div', { class: 'meta', text: item.lastRunSentinel ? 'last: ' + item.lastRunAction + ' → ' + item.lastRunSentinel : item.conditionReason }),
|
|
130
|
-
]));
|
|
131
|
-
return el('div', { class: 'col' }, [
|
|
132
|
-
el('h2', { text: cond + ' (' + items.length + ')' }),
|
|
133
|
-
...cards,
|
|
134
|
-
]);
|
|
135
|
-
}));
|
|
136
|
-
main.appendChild(columns);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function openItem(repo, number) {
|
|
140
|
-
const drawer = document.getElementById('drawer');
|
|
141
|
-
const body = document.getElementById('drawer-body');
|
|
142
|
-
body.innerHTML = 'Loading…';
|
|
143
|
-
drawer.classList.add('open');
|
|
144
|
-
const detail = await getJson('/items/' + encodeURIComponent(repo) + '/' + number);
|
|
145
|
-
if (!detail) { body.textContent = 'Not found'; return; }
|
|
146
|
-
body.innerHTML = '';
|
|
147
|
-
body.appendChild(el('h2', { text: repo + '#' + number }));
|
|
148
|
-
body.appendChild(el('p', { text: detail.item.issue.title }));
|
|
149
|
-
body.appendChild(el('p', { class: 'meta', text: 'stage: ' + detail.item.wake.stage + (detail.item.wake.sessionId ? ' · session: ' + detail.item.wake.sessionId : '') }));
|
|
150
|
-
if (detail.item.wake.workspacePath) {
|
|
151
|
-
body.appendChild(el('p', { class: 'meta', text: 'workspace: ' + detail.item.wake.workspacePath }));
|
|
152
|
-
}
|
|
153
|
-
body.appendChild(el('h3', { text: 'Runs' }));
|
|
154
|
-
const runsTable = el('table', {}, [
|
|
155
|
-
el('tr', {}, ['action', 'status', 'sentinel', 'started', 'runId'].map((h) => el('th', { text: h }))),
|
|
156
|
-
...detail.runs.map((r) => el('tr', {}, [
|
|
157
|
-
el('td', { text: r.action }), el('td', { text: r.status }), el('td', { text: r.sentinel || '' }),
|
|
158
|
-
el('td', { text: r.startedAt }), el('td', { text: r.runId }),
|
|
159
|
-
])),
|
|
160
|
-
]);
|
|
161
|
-
body.appendChild(runsTable);
|
|
162
|
-
body.appendChild(el('h3', { text: 'Context' }));
|
|
163
|
-
body.appendChild(el('pre', { text: JSON.stringify(detail.item.context, null, 2) }));
|
|
164
|
-
body.appendChild(el('h3', { text: 'Recent events' }));
|
|
165
|
-
body.appendChild(el('pre', { text: JSON.stringify(detail.events, null, 2) }));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function renderActivity() {
|
|
169
|
-
const events = await getJson('/events?limit=200');
|
|
170
|
-
const main = document.getElementById('main');
|
|
171
|
-
main.innerHTML = '';
|
|
172
|
-
const table = el('table', {}, [
|
|
173
|
-
el('tr', {}, ['time', 'direction', 'type', 'work item'].map((h) => el('th', { text: h }))),
|
|
174
|
-
...events.map((ev) => el('tr', {}, [
|
|
175
|
-
el('td', { text: ev.ingestedAt }), el('td', { text: ev.direction }),
|
|
176
|
-
el('td', { text: ev.sourceEventType }), el('td', { text: ev.workItemKey }),
|
|
177
|
-
])),
|
|
178
|
-
]);
|
|
179
|
-
main.appendChild(table);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function fmtCost(usd) {
|
|
183
|
-
if (usd === undefined || usd === null) return '';
|
|
184
|
-
return '$' + Number(usd).toFixed(usd < 1 ? 4 : 2);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function fmtTokens(tokenUsage) {
|
|
188
|
-
if (!tokenUsage) return '';
|
|
189
|
-
const total =
|
|
190
|
-
(tokenUsage.inputTokens || 0) + (tokenUsage.outputTokens || 0) +
|
|
191
|
-
(tokenUsage.cacheCreationInputTokens || 0) + (tokenUsage.cacheReadInputTokens || 0);
|
|
192
|
-
return total >= 1000 ? (total / 1000).toFixed(1) + 'k' : String(total);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function renderRuns() {
|
|
196
|
-
const runs = await getJson('/runs');
|
|
197
|
-
const main = document.getElementById('main');
|
|
198
|
-
main.innerHTML = '';
|
|
199
|
-
const table = el('table', {}, [
|
|
200
|
-
el('tr', {}, ['repo#issue', 'action', 'status', 'sentinel', 'runner', 'tokens', 'cost', 'started', 'finished'].map((h) => el('th', { text: h }))),
|
|
201
|
-
...runs.map((r) => el('tr', {}, [
|
|
202
|
-
el('td', { text: r.repo + '#' + r.issueNumber }), el('td', { text: r.action }), el('td', { text: r.status }),
|
|
203
|
-
el('td', { text: r.sentinel || '' }), el('td', { text: r.routing ? r.routing.runnerName : '' }),
|
|
204
|
-
el('td', { text: fmtTokens(r.tokenUsage) }), el('td', { text: fmtCost(r.tokenUsage && r.tokenUsage.costUsd) }),
|
|
205
|
-
el('td', { text: r.startedAt }), el('td', { text: r.finishedAt || '' }),
|
|
206
|
-
])),
|
|
207
|
-
]);
|
|
208
|
-
main.appendChild(table);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function renderConfig() {
|
|
212
|
-
const data = await getJson('/config');
|
|
213
|
-
const main = document.getElementById('main');
|
|
214
|
-
main.innerHTML = '';
|
|
215
|
-
main.appendChild(el('h3', { text: 'Routing table' }));
|
|
216
|
-
main.appendChild(el('table', {}, [
|
|
217
|
-
el('tr', {}, ['stage', 'action', 'tier', 'runner', 'model', 'fallback order'].map((h) => el('th', { text: h }))),
|
|
218
|
-
...data.routingTable.map((r) => el('tr', {}, [
|
|
219
|
-
el('td', { text: r.stage }), el('td', { text: r.action || '' }), el('td', { text: r.tier || '' }),
|
|
220
|
-
el('td', { text: r.runnerName || '' }), el('td', { text: r.model || '' }),
|
|
221
|
-
el('td', {}, (r.candidates || []).map((c) => el('span', {
|
|
222
|
-
class: 'chip' + (c.paused ? ' amber' : ''),
|
|
223
|
-
text: c.runnerName + (c.paused ? ' (paused)' : ''),
|
|
224
|
-
}))),
|
|
225
|
-
])),
|
|
226
|
-
]));
|
|
227
|
-
main.appendChild(el('h3', { text: 'Effective config (redacted)' }));
|
|
228
|
-
main.appendChild(el('pre', { text: JSON.stringify(data.config, null, 2) }));
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async function renderHealth() {
|
|
232
|
-
const health = await getJson('/health');
|
|
233
|
-
const main = document.getElementById('main');
|
|
234
|
-
main.innerHTML = '';
|
|
235
|
-
const runnerNames = Object.keys(health.pause.runnerHealth || {});
|
|
236
|
-
main.appendChild(el('div', { class: 'tiles' }, [
|
|
237
|
-
tile('Tick lock', health.lock.present ? (health.lock.stale ? 'stale' : 'held') : 'free'),
|
|
238
|
-
tile('Paused', String(health.pause.paused)),
|
|
239
|
-
tile('Runners paused now', String(runnerNames.filter((name) => {
|
|
240
|
-
const until = health.pause.runnerHealth[name].pausedUntil;
|
|
241
|
-
return until && Date.parse(until) > Date.now();
|
|
242
|
-
}).length)),
|
|
243
|
-
tile('Integrity issues', String(health.integrityIssues.length)),
|
|
244
|
-
]));
|
|
245
|
-
main.appendChild(el('h3', { text: 'Runner health (quota fallback, #67)' }));
|
|
246
|
-
if (runnerNames.length === 0) {
|
|
247
|
-
main.appendChild(el('p', { class: 'meta', text: 'No quota failures recorded.' }));
|
|
248
|
-
} else {
|
|
249
|
-
main.appendChild(el('table', {}, [
|
|
250
|
-
el('tr', {}, ['runner', 'status', 'paused until', 'failure count', 'last failure'].map((h) => el('th', { text: h }))),
|
|
251
|
-
...runnerNames.map((name) => {
|
|
252
|
-
const entry = health.pause.runnerHealth[name];
|
|
253
|
-
const paused = entry.pausedUntil && Date.parse(entry.pausedUntil) > Date.now();
|
|
254
|
-
return el('tr', {}, [
|
|
255
|
-
el('td', { text: name }),
|
|
256
|
-
el('td', {}, [el('span', { class: 'chip' + (paused ? ' amber' : ' ok') , text: paused ? 'paused' : 'available' })]),
|
|
257
|
-
el('td', { text: entry.pausedUntil || '' }),
|
|
258
|
-
el('td', { text: String(entry.failureCount || 0) }),
|
|
259
|
-
el('td', { text: entry.lastFailureAt || '' }),
|
|
260
|
-
]);
|
|
261
|
-
}),
|
|
262
|
-
]));
|
|
263
|
-
}
|
|
264
|
-
main.appendChild(el('h3', { text: 'Storage' }));
|
|
265
|
-
main.appendChild(el('pre', { text: JSON.stringify(health.storage, null, 2) }));
|
|
266
|
-
main.appendChild(el('h3', { text: 'Source polling' }));
|
|
267
|
-
main.appendChild(el('pre', { text: JSON.stringify(health.sources, null, 2) }));
|
|
268
|
-
main.appendChild(el('h3', { text: 'Integrity issues' }));
|
|
269
|
-
main.appendChild(el('pre', { text: JSON.stringify(health.integrityIssues, null, 2) }));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function tile(label, value) {
|
|
273
|
-
return el('div', { class: 'tile' }, [el('div', { class: 'n', text: value }), el('div', { class: 'l', text: label })]);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const renderers = { board: renderBoard, activity: renderActivity, runs: renderRuns, config: renderConfig, health: renderHealth };
|
|
277
|
-
|
|
278
|
-
function switchView(view) {
|
|
279
|
-
currentView = view;
|
|
280
|
-
for (const btn of document.querySelectorAll('nav button')) {
|
|
281
|
-
btn.classList.toggle('active', btn.dataset.view === view);
|
|
282
|
-
}
|
|
283
|
-
renderers[view]();
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
for (const btn of document.querySelectorAll('nav button')) {
|
|
287
|
-
btn.addEventListener('click', () => switchView(btn.dataset.view));
|
|
288
|
-
}
|
|
289
|
-
document.getElementById('drawer-close').addEventListener('click', () => {
|
|
290
|
-
document.getElementById('drawer').classList.remove('open');
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
renderStatusBar();
|
|
294
|
-
switchView('board');
|
|
295
|
-
setInterval(() => {
|
|
296
|
-
renderStatusBar();
|
|
297
|
-
renderers[currentView]();
|
|
298
|
-
}, 7000);
|
|
299
|
-
</script>
|
|
300
|
-
</body>
|
|
301
|
-
</html>
|
|
2
|
+
export const indexHtml = `<!DOCTYPE html>
|
|
3
|
+
<html lang="en">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<title>Wake control plane</title>
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
|
+
<style>
|
|
9
|
+
:root { color-scheme: dark light; }
|
|
10
|
+
body { font-family: -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; background: #14161a; color: #e8e8e8; }
|
|
11
|
+
header { display: flex; align-items: center; gap: 1rem; padding: 0.6rem 1rem; background: #1d2026; border-bottom: 1px solid #2c313a; flex-wrap: wrap; }
|
|
12
|
+
header h1 { font-size: 1rem; margin: 0; font-weight: 600; }
|
|
13
|
+
.version { color: #9aa2ad; font-size: 0.78rem; }
|
|
14
|
+
.pill { padding: 0.15rem 0.5rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; }
|
|
15
|
+
.pill-idle { background: #1f3d2c; color: #7fe3a3; }
|
|
16
|
+
.pill-ticking { background: #1f3350; color: #7fb3ff; }
|
|
17
|
+
.pill-paused { background: #4a3510; color: #ffcf7f; }
|
|
18
|
+
nav { display: flex; gap: 0.25rem; padding: 0.5rem 1rem; background: #191c22; border-bottom: 1px solid #2c313a; }
|
|
19
|
+
nav button { background: none; border: none; color: #9aa2ad; padding: 0.4rem 0.8rem; cursor: pointer; border-radius: 6px; font-size: 0.85rem; }
|
|
20
|
+
nav button.active { background: #262b33; color: #fff; }
|
|
21
|
+
main { padding: 1rem; }
|
|
22
|
+
.columns { display: grid; grid-template-columns: repeat(6, minmax(180px, 1fr)); gap: 0.6rem; overflow-x: auto; }
|
|
23
|
+
.col { background: #1a1d23; border-radius: 8px; padding: 0.5rem; min-height: 200px; }
|
|
24
|
+
.col h2 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: #9aa2ad; margin: 0.2rem 0.4rem 0.5rem; }
|
|
25
|
+
.card { background: #22262e; border: 1px solid #2c313a; border-radius: 6px; padding: 0.5rem; margin-bottom: 0.5rem; cursor: pointer; font-size: 0.8rem; }
|
|
26
|
+
.card:hover { border-color: #4d5866; }
|
|
27
|
+
.card .title { font-weight: 600; margin-bottom: 0.25rem; }
|
|
28
|
+
.card .meta { color: #9aa2ad; font-size: 0.72rem; }
|
|
29
|
+
.chip { display: inline-block; background: #2c313a; border-radius: 4px; padding: 0.05rem 0.35rem; font-size: 0.68rem; margin-right: 0.2rem; }
|
|
30
|
+
table { border-collapse: collapse; width: 100%; font-size: 0.8rem; }
|
|
31
|
+
th, td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid #2c313a; }
|
|
32
|
+
th { color: #9aa2ad; font-weight: 600; }
|
|
33
|
+
pre { background: #1a1d23; padding: 0.75rem; border-radius: 6px; overflow: auto; font-size: 0.75rem; }
|
|
34
|
+
.drawer { position: fixed; top: 0; right: 0; width: min(560px, 100%); height: 100%; background: #191c22; border-left: 1px solid #2c313a; overflow-y: auto; padding: 1rem; transform: translateX(100%); transition: transform 0.15s ease; }
|
|
35
|
+
.drawer.open { transform: translateX(0); }
|
|
36
|
+
.drawer .close { float: right; cursor: pointer; color: #9aa2ad; }
|
|
37
|
+
.tiles { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
|
38
|
+
.tile { background: #1a1d23; border-radius: 8px; padding: 0.6rem 0.9rem; min-width: 120px; }
|
|
39
|
+
.tile .n { font-size: 1.3rem; font-weight: 700; }
|
|
40
|
+
.tile .l { color: #9aa2ad; font-size: 0.72rem; text-transform: uppercase; }
|
|
41
|
+
.amber { color: #ffcf7f; }
|
|
42
|
+
.red { color: #ff8f7f; }
|
|
43
|
+
.ok { color: #7fe3a3; }
|
|
44
|
+
input[type=text] { background: #1a1d23; border: 1px solid #2c313a; color: #e8e8e8; padding: 0.3rem 0.5rem; border-radius: 6px; margin-bottom: 0.6rem; width: 260px; }
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
<header>
|
|
49
|
+
<h1>Wake control plane</h1>
|
|
50
|
+
<span class="version">${wakeVersion}</span>
|
|
51
|
+
<span id="loop-pill" class="pill">…</span>
|
|
52
|
+
<span id="status-summary" class="meta"></span>
|
|
53
|
+
</header>
|
|
54
|
+
<nav>
|
|
55
|
+
<button data-view="board" class="active">Board</button>
|
|
56
|
+
<button data-view="activity">Activity</button>
|
|
57
|
+
<button data-view="runs">Runs</button>
|
|
58
|
+
<button data-view="config">Config</button>
|
|
59
|
+
<button data-view="health">Health</button>
|
|
60
|
+
</nav>
|
|
61
|
+
<main id="main"></main>
|
|
62
|
+
<div id="drawer" class="drawer"><span class="close" id="drawer-close">close ✕</span><div id="drawer-body"></div></div>
|
|
63
|
+
<script>
|
|
64
|
+
const API = '/api/v1';
|
|
65
|
+
const CONDITIONS = ['needs-human', 'active', 'ready', 'waiting', 'stalled', 'finished'];
|
|
66
|
+
let currentView = 'board';
|
|
67
|
+
|
|
68
|
+
async function getJson(path) {
|
|
69
|
+
const res = await fetch(API + path);
|
|
70
|
+
if (!res.ok) throw new Error(path + ' -> ' + res.status);
|
|
71
|
+
return res.json();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fmtMs(ms) {
|
|
75
|
+
if (ms === undefined || ms === null) return '—';
|
|
76
|
+
const s = Math.floor(ms / 1000);
|
|
77
|
+
if (s < 60) return s + 's';
|
|
78
|
+
const m = Math.floor(s / 60);
|
|
79
|
+
if (m < 60) return m + 'm';
|
|
80
|
+
const h = Math.floor(m / 60);
|
|
81
|
+
if (h < 24) return h + 'h';
|
|
82
|
+
return Math.floor(h / 24) + 'd';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function el(tag, attrs, children) {
|
|
86
|
+
const node = document.createElement(tag);
|
|
87
|
+
for (const [k, v] of Object.entries(attrs || {})) {
|
|
88
|
+
if (k === 'text') node.textContent = v;
|
|
89
|
+
else if (k.startsWith('on')) node.addEventListener(k.slice(2), v);
|
|
90
|
+
else node.setAttribute(k, v);
|
|
91
|
+
}
|
|
92
|
+
for (const child of children || []) node.appendChild(child);
|
|
93
|
+
return node;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function renderStatusBar() {
|
|
97
|
+
try {
|
|
98
|
+
const status = await getJson('/status');
|
|
99
|
+
const pill = document.getElementById('loop-pill');
|
|
100
|
+
pill.textContent = status.loopState;
|
|
101
|
+
pill.className = 'pill pill-' + status.loopState;
|
|
102
|
+
const freshness = status.sourceFreshness.level;
|
|
103
|
+
const summary = document.getElementById('status-summary');
|
|
104
|
+
summary.textContent =
|
|
105
|
+
'runs today: ' + status.runsToday + ' · failures today: ' + status.failuresToday +
|
|
106
|
+
' · cost today: $' + Number(status.costUsdToday ?? 0).toFixed(2) +
|
|
107
|
+
' · source freshness: ' + freshness +
|
|
108
|
+
(status.lastRun ? ' · last run: ' + status.lastRun.repo + '#' + status.lastRun.issueNumber + ' ' + status.lastRun.action + ' → ' + (status.lastRun.sentinel ?? status.lastRun.status) : '');
|
|
109
|
+
} catch (err) {
|
|
110
|
+
document.getElementById('status-summary').textContent = 'status unavailable: ' + err.message;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function renderBoard() {
|
|
115
|
+
const board = await getJson('/board');
|
|
116
|
+
const main = document.getElementById('main');
|
|
117
|
+
main.innerHTML = '';
|
|
118
|
+
const columns = el('div', { class: 'columns' }, CONDITIONS.map((cond) => {
|
|
119
|
+
const items = board.filter((c) => c.condition === cond);
|
|
120
|
+
const cards = items.map((item) => el('div', {
|
|
121
|
+
class: 'card',
|
|
122
|
+
onclick: () => openItem(item.repo, item.number),
|
|
123
|
+
}, [
|
|
124
|
+
el('div', { class: 'title', text: item.repo + '#' + item.number + ' ' + item.title }),
|
|
125
|
+
el('div', { class: 'meta' }, [
|
|
126
|
+
el('span', { class: 'chip', text: item.stage }),
|
|
127
|
+
document.createTextNode(fmtMs(item.timeInStageMs) + ' in stage'),
|
|
128
|
+
]),
|
|
129
|
+
el('div', { class: 'meta', text: item.lastRunSentinel ? 'last: ' + item.lastRunAction + ' → ' + item.lastRunSentinel : item.conditionReason }),
|
|
130
|
+
]));
|
|
131
|
+
return el('div', { class: 'col' }, [
|
|
132
|
+
el('h2', { text: cond + ' (' + items.length + ')' }),
|
|
133
|
+
...cards,
|
|
134
|
+
]);
|
|
135
|
+
}));
|
|
136
|
+
main.appendChild(columns);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function openItem(repo, number) {
|
|
140
|
+
const drawer = document.getElementById('drawer');
|
|
141
|
+
const body = document.getElementById('drawer-body');
|
|
142
|
+
body.innerHTML = 'Loading…';
|
|
143
|
+
drawer.classList.add('open');
|
|
144
|
+
const detail = await getJson('/items/' + encodeURIComponent(repo) + '/' + number);
|
|
145
|
+
if (!detail) { body.textContent = 'Not found'; return; }
|
|
146
|
+
body.innerHTML = '';
|
|
147
|
+
body.appendChild(el('h2', { text: repo + '#' + number }));
|
|
148
|
+
body.appendChild(el('p', { text: detail.item.issue.title }));
|
|
149
|
+
body.appendChild(el('p', { class: 'meta', text: 'stage: ' + detail.item.wake.stage + (detail.item.wake.sessionId ? ' · session: ' + detail.item.wake.sessionId : '') }));
|
|
150
|
+
if (detail.item.wake.workspacePath) {
|
|
151
|
+
body.appendChild(el('p', { class: 'meta', text: 'workspace: ' + detail.item.wake.workspacePath }));
|
|
152
|
+
}
|
|
153
|
+
body.appendChild(el('h3', { text: 'Runs' }));
|
|
154
|
+
const runsTable = el('table', {}, [
|
|
155
|
+
el('tr', {}, ['action', 'status', 'sentinel', 'started', 'runId'].map((h) => el('th', { text: h }))),
|
|
156
|
+
...detail.runs.map((r) => el('tr', {}, [
|
|
157
|
+
el('td', { text: r.action }), el('td', { text: r.status }), el('td', { text: r.sentinel || '' }),
|
|
158
|
+
el('td', { text: r.startedAt }), el('td', { text: r.runId }),
|
|
159
|
+
])),
|
|
160
|
+
]);
|
|
161
|
+
body.appendChild(runsTable);
|
|
162
|
+
body.appendChild(el('h3', { text: 'Context' }));
|
|
163
|
+
body.appendChild(el('pre', { text: JSON.stringify(detail.item.context, null, 2) }));
|
|
164
|
+
body.appendChild(el('h3', { text: 'Recent events' }));
|
|
165
|
+
body.appendChild(el('pre', { text: JSON.stringify(detail.events, null, 2) }));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function renderActivity() {
|
|
169
|
+
const events = await getJson('/events?limit=200');
|
|
170
|
+
const main = document.getElementById('main');
|
|
171
|
+
main.innerHTML = '';
|
|
172
|
+
const table = el('table', {}, [
|
|
173
|
+
el('tr', {}, ['time', 'direction', 'type', 'work item'].map((h) => el('th', { text: h }))),
|
|
174
|
+
...events.map((ev) => el('tr', {}, [
|
|
175
|
+
el('td', { text: ev.ingestedAt }), el('td', { text: ev.direction }),
|
|
176
|
+
el('td', { text: ev.sourceEventType }), el('td', { text: ev.workItemKey }),
|
|
177
|
+
])),
|
|
178
|
+
]);
|
|
179
|
+
main.appendChild(table);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function fmtCost(usd) {
|
|
183
|
+
if (usd === undefined || usd === null) return '';
|
|
184
|
+
return '$' + Number(usd).toFixed(usd < 1 ? 4 : 2);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function fmtTokens(tokenUsage) {
|
|
188
|
+
if (!tokenUsage) return '';
|
|
189
|
+
const total =
|
|
190
|
+
(tokenUsage.inputTokens || 0) + (tokenUsage.outputTokens || 0) +
|
|
191
|
+
(tokenUsage.cacheCreationInputTokens || 0) + (tokenUsage.cacheReadInputTokens || 0);
|
|
192
|
+
return total >= 1000 ? (total / 1000).toFixed(1) + 'k' : String(total);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function renderRuns() {
|
|
196
|
+
const runs = await getJson('/runs');
|
|
197
|
+
const main = document.getElementById('main');
|
|
198
|
+
main.innerHTML = '';
|
|
199
|
+
const table = el('table', {}, [
|
|
200
|
+
el('tr', {}, ['repo#issue', 'action', 'status', 'sentinel', 'runner', 'tokens', 'cost', 'started', 'finished'].map((h) => el('th', { text: h }))),
|
|
201
|
+
...runs.map((r) => el('tr', {}, [
|
|
202
|
+
el('td', { text: r.repo + '#' + r.issueNumber }), el('td', { text: r.action }), el('td', { text: r.status }),
|
|
203
|
+
el('td', { text: r.sentinel || '' }), el('td', { text: r.routing ? r.routing.runnerName : '' }),
|
|
204
|
+
el('td', { text: fmtTokens(r.tokenUsage) }), el('td', { text: fmtCost(r.tokenUsage && r.tokenUsage.costUsd) }),
|
|
205
|
+
el('td', { text: r.startedAt }), el('td', { text: r.finishedAt || '' }),
|
|
206
|
+
])),
|
|
207
|
+
]);
|
|
208
|
+
main.appendChild(table);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function renderConfig() {
|
|
212
|
+
const data = await getJson('/config');
|
|
213
|
+
const main = document.getElementById('main');
|
|
214
|
+
main.innerHTML = '';
|
|
215
|
+
main.appendChild(el('h3', { text: 'Routing table' }));
|
|
216
|
+
main.appendChild(el('table', {}, [
|
|
217
|
+
el('tr', {}, ['stage', 'action', 'tier', 'runner', 'model', 'fallback order'].map((h) => el('th', { text: h }))),
|
|
218
|
+
...data.routingTable.map((r) => el('tr', {}, [
|
|
219
|
+
el('td', { text: r.stage }), el('td', { text: r.action || '' }), el('td', { text: r.tier || '' }),
|
|
220
|
+
el('td', { text: r.runnerName || '' }), el('td', { text: r.model || '' }),
|
|
221
|
+
el('td', {}, (r.candidates || []).map((c) => el('span', {
|
|
222
|
+
class: 'chip' + (c.paused ? ' amber' : ''),
|
|
223
|
+
text: c.runnerName + (c.paused ? ' (paused)' : ''),
|
|
224
|
+
}))),
|
|
225
|
+
])),
|
|
226
|
+
]));
|
|
227
|
+
main.appendChild(el('h3', { text: 'Effective config (redacted)' }));
|
|
228
|
+
main.appendChild(el('pre', { text: JSON.stringify(data.config, null, 2) }));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function renderHealth() {
|
|
232
|
+
const health = await getJson('/health');
|
|
233
|
+
const main = document.getElementById('main');
|
|
234
|
+
main.innerHTML = '';
|
|
235
|
+
const runnerNames = Object.keys(health.pause.runnerHealth || {});
|
|
236
|
+
main.appendChild(el('div', { class: 'tiles' }, [
|
|
237
|
+
tile('Tick lock', health.lock.present ? (health.lock.stale ? 'stale' : 'held') : 'free'),
|
|
238
|
+
tile('Paused', String(health.pause.paused)),
|
|
239
|
+
tile('Runners paused now', String(runnerNames.filter((name) => {
|
|
240
|
+
const until = health.pause.runnerHealth[name].pausedUntil;
|
|
241
|
+
return until && Date.parse(until) > Date.now();
|
|
242
|
+
}).length)),
|
|
243
|
+
tile('Integrity issues', String(health.integrityIssues.length)),
|
|
244
|
+
]));
|
|
245
|
+
main.appendChild(el('h3', { text: 'Runner health (quota fallback, #67)' }));
|
|
246
|
+
if (runnerNames.length === 0) {
|
|
247
|
+
main.appendChild(el('p', { class: 'meta', text: 'No quota failures recorded.' }));
|
|
248
|
+
} else {
|
|
249
|
+
main.appendChild(el('table', {}, [
|
|
250
|
+
el('tr', {}, ['runner', 'status', 'paused until', 'failure count', 'last failure'].map((h) => el('th', { text: h }))),
|
|
251
|
+
...runnerNames.map((name) => {
|
|
252
|
+
const entry = health.pause.runnerHealth[name];
|
|
253
|
+
const paused = entry.pausedUntil && Date.parse(entry.pausedUntil) > Date.now();
|
|
254
|
+
return el('tr', {}, [
|
|
255
|
+
el('td', { text: name }),
|
|
256
|
+
el('td', {}, [el('span', { class: 'chip' + (paused ? ' amber' : ' ok') , text: paused ? 'paused' : 'available' })]),
|
|
257
|
+
el('td', { text: entry.pausedUntil || '' }),
|
|
258
|
+
el('td', { text: String(entry.failureCount || 0) }),
|
|
259
|
+
el('td', { text: entry.lastFailureAt || '' }),
|
|
260
|
+
]);
|
|
261
|
+
}),
|
|
262
|
+
]));
|
|
263
|
+
}
|
|
264
|
+
main.appendChild(el('h3', { text: 'Storage' }));
|
|
265
|
+
main.appendChild(el('pre', { text: JSON.stringify(health.storage, null, 2) }));
|
|
266
|
+
main.appendChild(el('h3', { text: 'Source polling' }));
|
|
267
|
+
main.appendChild(el('pre', { text: JSON.stringify(health.sources, null, 2) }));
|
|
268
|
+
main.appendChild(el('h3', { text: 'Integrity issues' }));
|
|
269
|
+
main.appendChild(el('pre', { text: JSON.stringify(health.integrityIssues, null, 2) }));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function tile(label, value) {
|
|
273
|
+
return el('div', { class: 'tile' }, [el('div', { class: 'n', text: value }), el('div', { class: 'l', text: label })]);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const renderers = { board: renderBoard, activity: renderActivity, runs: renderRuns, config: renderConfig, health: renderHealth };
|
|
277
|
+
|
|
278
|
+
function switchView(view) {
|
|
279
|
+
currentView = view;
|
|
280
|
+
for (const btn of document.querySelectorAll('nav button')) {
|
|
281
|
+
btn.classList.toggle('active', btn.dataset.view === view);
|
|
282
|
+
}
|
|
283
|
+
renderers[view]();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const btn of document.querySelectorAll('nav button')) {
|
|
287
|
+
btn.addEventListener('click', () => switchView(btn.dataset.view));
|
|
288
|
+
}
|
|
289
|
+
document.getElementById('drawer-close').addEventListener('click', () => {
|
|
290
|
+
document.getElementById('drawer').classList.remove('open');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
renderStatusBar();
|
|
294
|
+
switchView('board');
|
|
295
|
+
setInterval(() => {
|
|
296
|
+
renderStatusBar();
|
|
297
|
+
renderers[currentView]();
|
|
298
|
+
}, 7000);
|
|
299
|
+
</script>
|
|
300
|
+
</body>
|
|
301
|
+
</html>
|
|
302
302
|
`;
|
package/dist/src/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const wakeVersion = "
|
|
1
|
+
export const wakeVersion = "0.1.3+g38d84e4";
|