@crewhaus/hangar-ui 0.5.0
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/README.md +309 -0
- package/assets/hangar.css +1726 -0
- package/assets/index.html +21 -0
- package/assets/js/api.js +368 -0
- package/assets/js/app.js +734 -0
- package/assets/js/dom.js +228 -0
- package/assets/js/keys.js +159 -0
- package/assets/js/markdown.js +131 -0
- package/assets/js/notify.js +152 -0
- package/assets/js/omnibox.js +307 -0
- package/assets/js/router.js +280 -0
- package/assets/js/routes.js +793 -0
- package/assets/js/shapes.js +36 -0
- package/assets/js/supervision.js +1334 -0
- package/assets/js/util.js +426 -0
- package/assets/js/views/activity.js +140 -0
- package/assets/js/views/approvals.js +155 -0
- package/assets/js/views/channels.js +455 -0
- package/assets/js/views/control.js +253 -0
- package/assets/js/views/costs.js +103 -0
- package/assets/js/views/creds.js +610 -0
- package/assets/js/views/data.js +456 -0
- package/assets/js/views/deploy.js +106 -0
- package/assets/js/views/evals-lab.js +1029 -0
- package/assets/js/views/evals.js +342 -0
- package/assets/js/views/feedback.js +555 -0
- package/assets/js/views/health.js +176 -0
- package/assets/js/views/inbox.js +103 -0
- package/assets/js/views/inspect.js +751 -0
- package/assets/js/views/jobs.js +110 -0
- package/assets/js/views/library.js +605 -0
- package/assets/js/views/memory-fabric.js +2026 -0
- package/assets/js/views/memory.js +228 -0
- package/assets/js/views/onboarding.js +253 -0
- package/assets/js/views/overview.js +196 -0
- package/assets/js/views/panes.js +132 -0
- package/assets/js/views/proc.js +280 -0
- package/assets/js/views/review.js +164 -0
- package/assets/js/views/runs.js +452 -0
- package/assets/js/views/runtime.js +450 -0
- package/assets/js/views/schedulers.js +133 -0
- package/assets/js/views/security.js +925 -0
- package/assets/js/views/sessions.js +265 -0
- package/assets/js/views/settings.js +489 -0
- package/assets/js/views/spec-edit.js +1289 -0
- package/assets/js/views/spec.js +66 -0
- package/assets/js/views/thredz.js +1853 -0
- package/assets/js/views/token.js +49 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +145 -0
- package/package.json +36 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Datasets — the dataset registry, its hygiene checks, and the growth verbs.
|
|
3
|
+
*
|
|
4
|
+
* Rules the screen carries rather than hides: a verify MISMATCH means
|
|
5
|
+
* TAMPERED (not stale); `<spec>-ratings` and `<spec>-regressions` are
|
|
6
|
+
* auto-maintained and invite no hand edit the next distill would overwrite;
|
|
7
|
+
* the test split is locked to the release flow with a visible burn count;
|
|
8
|
+
* and quarantined samples appear beside the registry with their provenance,
|
|
9
|
+
* because a quarantined sample nobody can see is a bug nobody can file.
|
|
10
|
+
*
|
|
11
|
+
* `dataset audit` (registry integrity) and `dataset lint` (canary
|
|
12
|
+
* contamination across specs and few-shot pools) answer different questions
|
|
13
|
+
* and get different buttons.
|
|
14
|
+
*
|
|
15
|
+
* Every node is built with el() — markup strings are banned app-wide, so a
|
|
16
|
+
* mined sample's text can never inject into this page.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { api } from "../api.js";
|
|
20
|
+
import { clear, collapsible, dot, el, emptyState, jsonPre, skeleton, toast } from "../dom.js";
|
|
21
|
+
import { fmtCount, fmtRelativeTime } from "../util.js";
|
|
22
|
+
|
|
23
|
+
export async function renderData(root, ctx) {
|
|
24
|
+
clear(root).appendChild(skeleton(4));
|
|
25
|
+
const [registry, status, quarantine] = await Promise.all(
|
|
26
|
+
[api.datasets, api.datasetStatus, api.datasetQuarantine].map((fn) =>
|
|
27
|
+
fn({ id: ctx.id }).catch((err) => ({ ok: false, status: 0, body: { error: String(err) } })),
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
clear(root);
|
|
31
|
+
const reload = () => renderData(root, ctx);
|
|
32
|
+
root.appendChild(registryCard(ctx, registry, reload));
|
|
33
|
+
root.appendChild(statusCard(status));
|
|
34
|
+
root.appendChild(quarantineCard(quarantine));
|
|
35
|
+
root.appendChild(hygieneCard(ctx));
|
|
36
|
+
root.appendChild(growthCard(ctx, reload));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// shared bits
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
function payload(res) {
|
|
44
|
+
return res && typeof res === "object" && res.body && typeof res.body === "object" ? res.body : {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function panel(title, res, draw) {
|
|
48
|
+
const data = payload(res);
|
|
49
|
+
const card = el("div", { class: "card" }, [
|
|
50
|
+
el("h3", { class: "card-title" }, [el("span", { text: title })]),
|
|
51
|
+
]);
|
|
52
|
+
if (res && res.ok === false && data.present === undefined) {
|
|
53
|
+
card.appendChild(el("p", { class: "muted", text: `unavailable (HTTP ${String(res.status)})` }));
|
|
54
|
+
return card;
|
|
55
|
+
}
|
|
56
|
+
if (data.present === false) {
|
|
57
|
+
card.appendChild(emptyState(String(data.note ?? "Nothing yet"), data.verb ?? null));
|
|
58
|
+
return card;
|
|
59
|
+
}
|
|
60
|
+
if (typeof data.note === "string" && data.note !== "") {
|
|
61
|
+
card.appendChild(el("p", { class: "muted", text: data.note }));
|
|
62
|
+
}
|
|
63
|
+
draw(card, data);
|
|
64
|
+
return card;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function table(headers, rows) {
|
|
68
|
+
return el("div", { class: "table-scroll" }, [
|
|
69
|
+
el("table", { class: "fleet" }, [
|
|
70
|
+
el(
|
|
71
|
+
"thead",
|
|
72
|
+
null,
|
|
73
|
+
el(
|
|
74
|
+
"tr",
|
|
75
|
+
null,
|
|
76
|
+
headers.map((h) => el("th", { text: h })),
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
el("tbody", null, rows),
|
|
80
|
+
]),
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function field(label, node) {
|
|
85
|
+
return el("label", { class: "field" }, [el("span", { class: "kv-k", text: label }), node]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function input(name, attrs) {
|
|
89
|
+
return el("input", { class: "input", name, ...(attrs ?? {}) });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readText(form, name) {
|
|
93
|
+
const raw = form.elements[name]?.value?.trim();
|
|
94
|
+
return raw ? raw : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function call(fn, params, body) {
|
|
98
|
+
return await fn(params, body).catch((err) => ({
|
|
99
|
+
ok: false,
|
|
100
|
+
status: 0,
|
|
101
|
+
body: { error: String(err) },
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Split sizes as one string, with the locked holdout marked. */
|
|
106
|
+
function splitsText(splits) {
|
|
107
|
+
if (!splits) return "—";
|
|
108
|
+
const test =
|
|
109
|
+
splits.test === null || splits.test === undefined ? "no test" : `${splits.test} test 🔒`;
|
|
110
|
+
return `${fmtCount(splits.train)} train · ${fmtCount(splits.dev)} dev · ${test}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Provenance is a TAXONOMY — render the tally, not a free-text note. */
|
|
114
|
+
function provenanceChips(tally) {
|
|
115
|
+
const entries = Object.entries(tally ?? {});
|
|
116
|
+
if (entries.length === 0) return el("span", { class: "muted", text: "—" });
|
|
117
|
+
return el(
|
|
118
|
+
"span",
|
|
119
|
+
{ class: "cell-caps" },
|
|
120
|
+
entries.map(([source, count]) =>
|
|
121
|
+
el("span", {
|
|
122
|
+
// A canary sample among ordinary rows is what `dataset lint` hunts.
|
|
123
|
+
class: source === "canary" || source === "(unlabelled)" ? "chip chip-warn" : "chip",
|
|
124
|
+
text: `${source} ${String(count)}`,
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** A verify result is binary and its language matters: a mismatch means the
|
|
131
|
+
* stored hashes no longer describe the content — TAMPERED, never "stale". */
|
|
132
|
+
function verifyDot(verify) {
|
|
133
|
+
if (!verify) return dot("unknown", "not verified");
|
|
134
|
+
return verify.ok ? dot("ok", "intact") : dot("bad", `tampered (${fmtCount(verify.mismatches)})`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// the registry
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
function registryCard(ctx, res, reload) {
|
|
142
|
+
return panel("Dataset registry", res, (card, data) => {
|
|
143
|
+
const rows = (data.datasets ?? []).map((row) =>
|
|
144
|
+
el("tr", null, [
|
|
145
|
+
el("td", null, [
|
|
146
|
+
el("span", { class: "mono", text: String(row.name) }),
|
|
147
|
+
row.autoMaintained
|
|
148
|
+
? el("span", {
|
|
149
|
+
class: "chip chip-group",
|
|
150
|
+
title: String(row.autoMaintained),
|
|
151
|
+
text: "auto-maintained",
|
|
152
|
+
})
|
|
153
|
+
: null,
|
|
154
|
+
]),
|
|
155
|
+
el("td", { class: "mono", text: String(row.latestVersion ?? "—") }),
|
|
156
|
+
el("td", { text: splitsText(row.splits) }),
|
|
157
|
+
el("td", null, [provenanceChips(row.provenance)]),
|
|
158
|
+
el("td", null, [verifyDot(row.verify)]),
|
|
159
|
+
el("td", { class: "num", text: fmtCount(row.testSplitBurn) }),
|
|
160
|
+
el("td", { class: "cell-actions" }, [
|
|
161
|
+
detailBtn(ctx, row.name),
|
|
162
|
+
verifyBtn(ctx, row.name, reload),
|
|
163
|
+
]),
|
|
164
|
+
]),
|
|
165
|
+
);
|
|
166
|
+
card.appendChild(
|
|
167
|
+
table(["Dataset", "Latest", "Splits", "Provenance", "Verify", "Holdout burn", ""], rows),
|
|
168
|
+
);
|
|
169
|
+
card.appendChild(
|
|
170
|
+
el("p", {
|
|
171
|
+
class: "muted",
|
|
172
|
+
text: "The held-out test split is locked to the release flow; every spend is counted as a burn.",
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function detailBtn(ctx, name) {
|
|
179
|
+
const holder = el("div");
|
|
180
|
+
const btn = el("button", { class: "btn btn-ghost", type: "button", text: "Versions" });
|
|
181
|
+
btn.addEventListener("click", async () => {
|
|
182
|
+
btn.disabled = true;
|
|
183
|
+
const res = await call(api.dataset, { id: ctx.id, name: String(name) });
|
|
184
|
+
btn.disabled = false;
|
|
185
|
+
clear(holder);
|
|
186
|
+
if (!res.ok) {
|
|
187
|
+
toast(String(payload(res).error ?? `HTTP ${String(res.status)}`), "error");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const data = payload(res);
|
|
191
|
+
if (data.present === false) {
|
|
192
|
+
holder.appendChild(emptyState(String(data.note), data.verb ?? null));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
holder.appendChild(
|
|
196
|
+
table(
|
|
197
|
+
["Version", "Created", "Splits", "Verify", "Burn"],
|
|
198
|
+
(data.versions ?? []).map((v) =>
|
|
199
|
+
el("tr", null, [
|
|
200
|
+
el("td", { class: "mono", text: String(v.version) }),
|
|
201
|
+
el("td", { text: fmtRelativeTime(v.createdAt, Date.now()) }),
|
|
202
|
+
el("td", { text: splitsText(v.splits) }),
|
|
203
|
+
el("td", null, [verifyDot(v.verify)]),
|
|
204
|
+
el("td", { class: "num", text: fmtCount(v.burn?.count) }),
|
|
205
|
+
]),
|
|
206
|
+
),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
if (data.testSplit) {
|
|
210
|
+
holder.appendChild(el("p", { class: "gated-why", text: String(data.testSplit.note) }));
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
return el("div", { class: "job-action" }, [btn, holder]);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function verifyBtn(ctx, name, reload) {
|
|
217
|
+
const btn = el("button", { class: "btn btn-ghost", type: "button", text: "Verify" });
|
|
218
|
+
btn.addEventListener("click", async () => {
|
|
219
|
+
btn.disabled = true;
|
|
220
|
+
const res = await call(api.datasetVerify, { id: ctx.id }, { name: String(name) });
|
|
221
|
+
btn.disabled = false;
|
|
222
|
+
if (!res.ok) {
|
|
223
|
+
toast(String(payload(res).error ?? `HTTP ${String(res.status)}`), "error");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const tampered = payload(res).tampered ?? [];
|
|
227
|
+
toast(
|
|
228
|
+
tampered.length === 0
|
|
229
|
+
? `${String(name)}: every version matches its stored hashes`
|
|
230
|
+
: `${String(name)}: TAMPERED — ${tampered.join(", ")}`,
|
|
231
|
+
tampered.length === 0 ? "info" : "error",
|
|
232
|
+
);
|
|
233
|
+
if (reload) reload();
|
|
234
|
+
});
|
|
235
|
+
return btn;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// status + quarantine
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
function statusCard(res) {
|
|
243
|
+
return panel("Freshness, saturation and burn", res, (card, data) => {
|
|
244
|
+
card.appendChild(
|
|
245
|
+
table(
|
|
246
|
+
["Dataset", "Version", "Age", "Runs", "Last run", "Always-passing", "Burn"],
|
|
247
|
+
(data.datasets ?? []).map((row) => {
|
|
248
|
+
const saturated = row.saturation?.alwaysPassing ?? [];
|
|
249
|
+
return el("tr", null, [
|
|
250
|
+
el("td", { class: "mono", text: String(row.name) }),
|
|
251
|
+
el("td", { class: "mono", text: String(row.version) }),
|
|
252
|
+
el("td", {
|
|
253
|
+
class: "num",
|
|
254
|
+
text: row.ageDays === null ? "—" : `${String(row.ageDays)}d`,
|
|
255
|
+
}),
|
|
256
|
+
el("td", { class: "num", text: fmtCount(row.runCount) }),
|
|
257
|
+
el("td", { text: fmtRelativeTime(row.lastRunTs, Date.now()) }),
|
|
258
|
+
el("td", null, [
|
|
259
|
+
saturated.length === 0
|
|
260
|
+
? dot("ok", "discriminating")
|
|
261
|
+
: dot("warn", `${String(saturated.length)} saturated`),
|
|
262
|
+
]),
|
|
263
|
+
el("td", { class: "num", text: fmtCount(row.testSplitBurn) }),
|
|
264
|
+
]);
|
|
265
|
+
}),
|
|
266
|
+
),
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function quarantineCard(res) {
|
|
272
|
+
return panel("Quarantine", res, (card, data) => {
|
|
273
|
+
card.appendChild(
|
|
274
|
+
table(
|
|
275
|
+
["Sample", "Source", "Why it was pulled", "File"],
|
|
276
|
+
(data.entries ?? []).map((entry) =>
|
|
277
|
+
el("tr", null, [
|
|
278
|
+
el("td", { class: "mono", text: String(entry.id ?? "—") }),
|
|
279
|
+
el("td", null, [
|
|
280
|
+
el("span", { class: "chip", text: String(entry.source ?? "(unlabelled)") }),
|
|
281
|
+
]),
|
|
282
|
+
el("td", { class: "muted", text: String(entry.reason ?? entry.input ?? "") }),
|
|
283
|
+
el("td", { class: "mono muted", text: String(entry.file) }),
|
|
284
|
+
]),
|
|
285
|
+
),
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
if (data.promoteVerb) {
|
|
289
|
+
card.appendChild(
|
|
290
|
+
el("p", { class: "muted" }, [
|
|
291
|
+
el("span", { text: "Promote accepted candidates with " }),
|
|
292
|
+
el("code", { text: String(data.promoteVerb) }),
|
|
293
|
+
]),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// hygiene — two DIFFERENT questions, two buttons
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
function hygieneCard(ctx) {
|
|
304
|
+
const out = el("div");
|
|
305
|
+
const card = el("div", { class: "card" }, [
|
|
306
|
+
el("h3", { class: "card-title" }, [el("span", { text: "Hygiene" })]),
|
|
307
|
+
el("p", {
|
|
308
|
+
class: "muted",
|
|
309
|
+
text: "Audit asks whether the registry is internally consistent. Lint asks whether canary samples have leaked into the spec or the few-shot pool. They are not the same check.",
|
|
310
|
+
}),
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
const audit = el("button", { class: "btn btn-ghost", type: "button", text: "Audit registry" });
|
|
314
|
+
audit.addEventListener("click", async () => {
|
|
315
|
+
audit.disabled = true;
|
|
316
|
+
const res = await call(api.datasetAudit, { id: ctx.id }, {});
|
|
317
|
+
audit.disabled = false;
|
|
318
|
+
clear(out);
|
|
319
|
+
if (!res.ok) {
|
|
320
|
+
toast(String(payload(res).error ?? `HTTP ${String(res.status)}`), "error");
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const data = payload(res);
|
|
324
|
+
const findings = data.findings ?? [];
|
|
325
|
+
if (findings.length === 0) {
|
|
326
|
+
out.appendChild(emptyState("No integrity findings", null));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
out.appendChild(
|
|
330
|
+
table(
|
|
331
|
+
["Level", "Dataset", "Version", "Finding"],
|
|
332
|
+
findings.map((f) =>
|
|
333
|
+
el("tr", null, [
|
|
334
|
+
el("td", null, [
|
|
335
|
+
dot(
|
|
336
|
+
f.level === "error" ? "bad" : f.level === "warn" ? "warn" : "unknown",
|
|
337
|
+
String(f.level),
|
|
338
|
+
),
|
|
339
|
+
]),
|
|
340
|
+
el("td", { class: "mono", text: String(f.name) }),
|
|
341
|
+
el("td", { class: "mono", text: String(f.version ?? "—") }),
|
|
342
|
+
el("td", { text: String(f.finding) }),
|
|
343
|
+
]),
|
|
344
|
+
),
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const lint = el("button", {
|
|
350
|
+
class: "btn btn-ghost",
|
|
351
|
+
type: "button",
|
|
352
|
+
text: "Scan for contamination",
|
|
353
|
+
});
|
|
354
|
+
lint.addEventListener("click", async () => {
|
|
355
|
+
lint.disabled = true;
|
|
356
|
+
const res = await call(api.datasetLint, { id: ctx.id }, {});
|
|
357
|
+
lint.disabled = false;
|
|
358
|
+
if (!res.ok) {
|
|
359
|
+
toast(String(payload(res).error ?? `HTTP ${String(res.status)}`), "error");
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
toast("Contamination scan queued — watch the job feed", "info");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
card.appendChild(el("div", { class: "editor-actions" }, [audit, lint]));
|
|
366
|
+
card.appendChild(out);
|
|
367
|
+
return card;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// growth — preview, THEN write
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Each growth verb adds rows to a dataset that later gates a release, so the
|
|
376
|
+
* default is a preview: what would run, where it would write, and what
|
|
377
|
+
* review still stands between the output and a gating dataset.
|
|
378
|
+
*/
|
|
379
|
+
function growthCard(ctx, reload) {
|
|
380
|
+
const out = el("div");
|
|
381
|
+
const form = el("form", { class: "row-editor" }, [
|
|
382
|
+
field("dataset", input("name", { placeholder: "dataset name", required: true })),
|
|
383
|
+
field(
|
|
384
|
+
"verb",
|
|
385
|
+
el(
|
|
386
|
+
"select",
|
|
387
|
+
{ class: "input", name: "verb" },
|
|
388
|
+
[
|
|
389
|
+
["mine", "mine hard cases from sessions"],
|
|
390
|
+
["synthesize", "synthesize stress variants"],
|
|
391
|
+
["refresh-goldens", "refresh goldens"],
|
|
392
|
+
].map(([value, label]) => el("option", { value, text: label })),
|
|
393
|
+
),
|
|
394
|
+
),
|
|
395
|
+
]);
|
|
396
|
+
const preview = el("button", { class: "btn", type: "submit", text: "Preview" });
|
|
397
|
+
form.appendChild(el("div", { class: "editor-actions" }, [preview]));
|
|
398
|
+
|
|
399
|
+
const verbApi = {
|
|
400
|
+
mine: api.datasetMine,
|
|
401
|
+
synthesize: api.datasetSynthesize,
|
|
402
|
+
"refresh-goldens": api.datasetRefreshGoldens,
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
form.addEventListener("submit", async (event) => {
|
|
406
|
+
event.preventDefault();
|
|
407
|
+
const name = readText(form, "name");
|
|
408
|
+
const verb = form.elements["verb"].value;
|
|
409
|
+
preview.disabled = true;
|
|
410
|
+
const res = await call(verbApi[verb], { id: ctx.id }, { name, dryRun: true });
|
|
411
|
+
preview.disabled = false;
|
|
412
|
+
clear(out);
|
|
413
|
+
if (!res.ok) {
|
|
414
|
+
toast(String(payload(res).error ?? `HTTP ${String(res.status)}`), "error");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const data = payload(res);
|
|
418
|
+
const plan = data.plan ?? {};
|
|
419
|
+
out.appendChild(el("p", { class: "muted", text: String(data.note ?? "") }));
|
|
420
|
+
out.appendChild(el("p", { class: "gated-why", text: String(plan.reviewGate ?? "") }));
|
|
421
|
+
out.appendChild(
|
|
422
|
+
collapsible(
|
|
423
|
+
[el("span", { class: "muted", text: "what this will run and where it writes" })],
|
|
424
|
+
[jsonPre({ argv: plan.argv ?? [], writes: plan.writes ?? [] })],
|
|
425
|
+
true,
|
|
426
|
+
),
|
|
427
|
+
);
|
|
428
|
+
const run = el("button", {
|
|
429
|
+
class: "btn btn-danger",
|
|
430
|
+
type: "button",
|
|
431
|
+
text: `Run ${verb} for real`,
|
|
432
|
+
});
|
|
433
|
+
run.addEventListener("click", async () => {
|
|
434
|
+
run.disabled = true;
|
|
435
|
+
const applied = await call(verbApi[verb], { id: ctx.id }, { name, dryRun: false });
|
|
436
|
+
run.disabled = false;
|
|
437
|
+
if (!applied.ok) {
|
|
438
|
+
toast(String(payload(applied).error ?? `HTTP ${String(applied.status)}`), "error");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
toast(`${verb} queued`, "info");
|
|
442
|
+
if (reload) reload();
|
|
443
|
+
});
|
|
444
|
+
out.appendChild(el("div", { class: "editor-actions" }, [run]));
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return el("div", { class: "card" }, [
|
|
448
|
+
el("h3", { class: "card-title" }, [el("span", { text: "Grow a dataset" })]),
|
|
449
|
+
el("p", {
|
|
450
|
+
class: "muted",
|
|
451
|
+
text: "Mining stages candidates in quarantine; synthetic rows never become gold; refreshing goldens changes what correct means. Each previews first.",
|
|
452
|
+
}),
|
|
453
|
+
form,
|
|
454
|
+
out,
|
|
455
|
+
]);
|
|
456
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployments — the records a harness has written to
|
|
3
|
+
* `.crewhaus/deployments.json`, with their class (local / PaaS / cf-worker)
|
|
4
|
+
* and health.
|
|
5
|
+
*
|
|
6
|
+
* HONESTLY EMPTY IS THE NORMAL ANSWER. Nothing writes that file yet, and the
|
|
7
|
+
* manager deliberately does not invent a writer: a console that "knows"
|
|
8
|
+
* about a deploy it did not perform — and misses every deploy performed from
|
|
9
|
+
* a terminal — is worse than one that says the file is absent and points at
|
|
10
|
+
* where it will appear. So the empty state names the path, not a failure.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { api } from "../api.js";
|
|
14
|
+
import { clear, copyBtn, dot, el, emptyState, jsonPre, skeleton } from "../dom.js";
|
|
15
|
+
import { deploymentRow } from "../supervision.js";
|
|
16
|
+
|
|
17
|
+
export async function renderDeploy(root, ctx) {
|
|
18
|
+
clear(root).appendChild(skeleton(4));
|
|
19
|
+
const view = await api.deployments(ctx.id);
|
|
20
|
+
clear(root);
|
|
21
|
+
if (view === null) {
|
|
22
|
+
root.appendChild(emptyState("Nothing here yet — no deployment records"));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const nowMs = Date.now();
|
|
26
|
+
const records = Array.isArray(view.deployments) ? view.deployments : [];
|
|
27
|
+
const path = typeof view.path === "string" ? view.path : "";
|
|
28
|
+
|
|
29
|
+
if (view.error !== null && view.error !== undefined) {
|
|
30
|
+
root.appendChild(
|
|
31
|
+
el("div", { class: "card error-card" }, [
|
|
32
|
+
el("h3", { class: "card-title" }, [dot("bad", "deployments.json is unreadable")]),
|
|
33
|
+
el("p", { class: "muted", text: String(view.error) }),
|
|
34
|
+
el("div", { class: "mono muted", text: path }),
|
|
35
|
+
]),
|
|
36
|
+
);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (records.length === 0) {
|
|
41
|
+
root.appendChild(
|
|
42
|
+
el("div", { class: "card" }, [
|
|
43
|
+
el("h3", { class: "card-title" }, [
|
|
44
|
+
el("span", { text: "Deployments" }),
|
|
45
|
+
dot("off", view.present === true ? "file present, no records" : "nothing recorded"),
|
|
46
|
+
]),
|
|
47
|
+
emptyState(
|
|
48
|
+
typeof view.note === "string" && view.note !== ""
|
|
49
|
+
? view.note
|
|
50
|
+
: "No deployment records yet",
|
|
51
|
+
),
|
|
52
|
+
el("div", { class: "h-dir" }, [
|
|
53
|
+
el("span", { class: "mono muted", title: path, text: path }),
|
|
54
|
+
path !== "" ? copyBtn(path, "copy path") : null,
|
|
55
|
+
]),
|
|
56
|
+
]),
|
|
57
|
+
);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const tbody = el("tbody");
|
|
62
|
+
for (const record of records) {
|
|
63
|
+
const row = deploymentRow(record, nowMs);
|
|
64
|
+
tbody.appendChild(
|
|
65
|
+
el("tr", null, [
|
|
66
|
+
el("td", { text: row.env }),
|
|
67
|
+
el("td", null, [el("span", { class: "chip", text: row.klass })]),
|
|
68
|
+
el("td", { class: "mono", text: row.provider ?? "—" }),
|
|
69
|
+
el("td", { class: "mono", text: row.version ?? "—" }),
|
|
70
|
+
el("td", { text: row.when }),
|
|
71
|
+
el("td", null, [dot(row.dot, row.healthLabel)]),
|
|
72
|
+
el("td", null, [
|
|
73
|
+
row.url !== null
|
|
74
|
+
? el("a", { class: "mono", href: row.url, rel: "noreferrer", text: row.url })
|
|
75
|
+
: el("span", { class: "muted", text: "—" }),
|
|
76
|
+
]),
|
|
77
|
+
]),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
root.appendChild(
|
|
81
|
+
el("div", { class: "table-scroll" }, [
|
|
82
|
+
el("table", { class: "fleet" }, [
|
|
83
|
+
el(
|
|
84
|
+
"thead",
|
|
85
|
+
null,
|
|
86
|
+
el(
|
|
87
|
+
"tr",
|
|
88
|
+
null,
|
|
89
|
+
["Env", "Class", "Provider", "Version", "When", "Health", "URL"].map((h) =>
|
|
90
|
+
el("th", { text: h }),
|
|
91
|
+
),
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
tbody,
|
|
95
|
+
]),
|
|
96
|
+
]),
|
|
97
|
+
);
|
|
98
|
+
root.appendChild(
|
|
99
|
+
el("details", { class: "fold" }, [
|
|
100
|
+
el("summary", { class: "fold-summary" }, [
|
|
101
|
+
el("span", { class: "muted", text: `raw records — ${path}` }),
|
|
102
|
+
]),
|
|
103
|
+
el("div", { class: "fold-body" }, [jsonPre(records)]),
|
|
104
|
+
]),
|
|
105
|
+
);
|
|
106
|
+
}
|