@crewhaus/hangar-ui 0.5.2 → 0.5.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/assets/js/api.js +3 -0
- package/assets/js/routes.js +11 -0
- package/assets/js/views/runs.js +121 -1
- package/package.json +1 -1
package/assets/js/api.js
CHANGED
|
@@ -257,6 +257,9 @@ export const api = {
|
|
|
257
257
|
addGroup: (name) => call(ROUTES.addGroup, {}, { name }),
|
|
258
258
|
addScanRoot: (dir) => call(ROUTES.addScanRoot, {}, { dir }),
|
|
259
259
|
setGroups: (id, groups) => call(ROUTES.setGroups, { id }, { groups }),
|
|
260
|
+
setGroupOrder: (id, group, order) => call(ROUTES.setGroupOrder, { id }, { group, order }),
|
|
261
|
+
groupProcPlan: (name, verb) => call(ROUTES.groupProcPlan, { name, verb }),
|
|
262
|
+
groupProc: (name, verb, body = {}) => call(ROUTES.groupProc, { name, verb }, body),
|
|
260
263
|
setTags: (id, tags) => call(ROUTES.setTags, { id }, { tags }),
|
|
261
264
|
setPin: (id, pinned) => call(ROUTES.setPin, { id }, { pinned }),
|
|
262
265
|
setNotes: (id, notes) => call(ROUTES.setNotes, { id }, { notes }),
|
package/assets/js/routes.js
CHANGED
|
@@ -46,10 +46,21 @@ export const ROUTES = {
|
|
|
46
46
|
scan: { method: "POST", path: "/api/scan", body: "Empty" }, // {}
|
|
47
47
|
groups: { method: "GET", path: "/api/registry/groups" },
|
|
48
48
|
addGroup: { method: "POST", path: "/api/registry/groups", body: "GroupCreate" }, // {name, color?}
|
|
49
|
+
// Group-ordered bulk lifecycle. PLAN first, then act: a fleet sweep is the
|
|
50
|
+
// one lifecycle action an operator should read before authorizing, and the
|
|
51
|
+
// plan is the same object the walk consumes.
|
|
52
|
+
groupProcPlan: { method: "GET", path: "/api/registry/groups/:name/proc/:verb" },
|
|
53
|
+
groupProc: {
|
|
54
|
+
method: "POST",
|
|
55
|
+
path: "/api/registry/groups/:name/proc/:verb",
|
|
56
|
+
body: "GroupProc", // {force?, acknowledge?, parallel?}
|
|
57
|
+
},
|
|
49
58
|
addScanRoot: { method: "POST", path: "/api/registry/scan-roots", body: "ScanRootCreate" }, // {dir}
|
|
50
59
|
removeHarness: { method: "DELETE", path: "/api/h/:id" },
|
|
51
60
|
relocate: { method: "POST", path: "/api/h/:id/relocate", body: "Relocate" }, // {newDir}
|
|
52
61
|
setGroups: { method: "PUT", path: "/api/h/:id/groups", body: "SetGroups" }, // {groups}
|
|
62
|
+
// One member's BOOT position inside a group; `order: null` clears it.
|
|
63
|
+
setGroupOrder: { method: "PUT", path: "/api/h/:id/groups", body: "SetGroupOrder" }, // {group, order}
|
|
53
64
|
setTags: { method: "PUT", path: "/api/h/:id/tags", body: "SetTags" }, // {tags}
|
|
54
65
|
setPin: { method: "PUT", path: "/api/h/:id/pin", body: "SetPin" }, // {pinned}
|
|
55
66
|
setNotes: { method: "PUT", path: "/api/h/:id/notes", body: "SetNotes" }, // {notes}
|
package/assets/js/views/runs.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { api, streamRunEvents } from "../api.js";
|
|
22
|
-
import { clear, collapsible, dot, el, emptyState, jsonPre, skeleton } from "../dom.js";
|
|
22
|
+
import { clear, collapsible, dot, el, emptyState, jsonPre, skeleton, toast } from "../dom.js";
|
|
23
23
|
import { hrefHarness } from "../router.js";
|
|
24
24
|
import { shapeAccent, shapeLabel } from "../shapes.js";
|
|
25
25
|
import {
|
|
@@ -66,6 +66,13 @@ export async function renderRunsBoard(root) {
|
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Groups first: a fleet is brought up as a fleet, in the order its members
|
|
70
|
+
// declare, and doing that a row at a time was N clicks in an order the
|
|
71
|
+
// operator had to remember.
|
|
72
|
+
const groupsCard = el("section", { class: "card" });
|
|
73
|
+
root.appendChild(groupsCard);
|
|
74
|
+
void renderGroupStrip(groupsCard, () => renderRunsBoard(root));
|
|
75
|
+
|
|
69
76
|
const tbody = el("tbody");
|
|
70
77
|
const table = el("div", { class: "table-scroll" }, [
|
|
71
78
|
el("table", { class: "fleet" }, [
|
|
@@ -158,6 +165,119 @@ export async function renderRunsBoard(root) {
|
|
|
158
165
|
}
|
|
159
166
|
}
|
|
160
167
|
|
|
168
|
+
/**
|
|
169
|
+
* The group strip — group-ordered bulk lifecycle, PLAN FIRST.
|
|
170
|
+
*
|
|
171
|
+
* A fleet has dependency order (the secretary everyone's A2A door mounts
|
|
172
|
+
* boots first; the chief that supervises the rest last), and the board was
|
|
173
|
+
* per-row Start/Stop, so bringing one up was N clicks in an order the
|
|
174
|
+
* operator had to remember. Clicking a verb here does NOT act: it fetches
|
|
175
|
+
* the walk and renders it, in order, with every skip named — and only the
|
|
176
|
+
* second click runs it. A sweep that touches a whole fleet should be
|
|
177
|
+
* readable before it is authorized.
|
|
178
|
+
*/
|
|
179
|
+
async function renderGroupStrip(card, reload) {
|
|
180
|
+
clear(card).appendChild(el("h3", { class: "card-title", text: "Groups" }));
|
|
181
|
+
let groups = [];
|
|
182
|
+
try {
|
|
183
|
+
groups = (await api.groups())?.groups ?? [];
|
|
184
|
+
} catch {
|
|
185
|
+
// The board is the point; a registry read that fails leaves the strip
|
|
186
|
+
// out rather than taking the page down.
|
|
187
|
+
card.remove();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (groups.length === 0) {
|
|
191
|
+
card.appendChild(
|
|
192
|
+
emptyState("No groups yet", "crewhaus harness group <name> --add <dir> --member-order <n>"),
|
|
193
|
+
);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
for (const group of groups) {
|
|
197
|
+
const body = el("div", { class: "muted", text: "pick a verb to see the planned order" });
|
|
198
|
+
card.appendChild(
|
|
199
|
+
el("div", { class: "group-row" }, [
|
|
200
|
+
el("div", { class: "row-head" }, [
|
|
201
|
+
el("strong", { text: group.name }),
|
|
202
|
+
el("span", { class: "muted", text: `order ${group.order}` }),
|
|
203
|
+
...["start", "restart", "stop"].map((verb) =>
|
|
204
|
+
el("button", {
|
|
205
|
+
class: "btn",
|
|
206
|
+
text: verb,
|
|
207
|
+
onclick: () => void showGroupPlan(body, group.name, verb, reload),
|
|
208
|
+
}),
|
|
209
|
+
),
|
|
210
|
+
]),
|
|
211
|
+
body,
|
|
212
|
+
]),
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Render the walk a verb WOULD take, with a confirm that runs it. */
|
|
218
|
+
async function showGroupPlan(body, name, verb, reload) {
|
|
219
|
+
clear(body).appendChild(el("p", { class: "muted", text: "planning…" }));
|
|
220
|
+
let plan = null;
|
|
221
|
+
try {
|
|
222
|
+
plan = (await api.groupProcPlan(name, verb))?.plan ?? null;
|
|
223
|
+
} catch (err) {
|
|
224
|
+
clear(body).appendChild(el("p", { class: "error", text: String(err?.message ?? err) }));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (plan === null || plan.members.length === 0) {
|
|
228
|
+
clear(body).appendChild(el("p", { class: "muted", text: `group ${name} has no members` }));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
clear(body);
|
|
232
|
+
body.appendChild(
|
|
233
|
+
el("p", {
|
|
234
|
+
text: `${verb} would walk ${plan.actionable} member(s)${
|
|
235
|
+
verb === "stop" ? " in REVERSE boot order" : " in boot order"
|
|
236
|
+
}${plan.skipped > 0 ? `, skipping ${plan.skipped}` : ""}:`,
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
body.appendChild(
|
|
240
|
+
el(
|
|
241
|
+
"ol",
|
|
242
|
+
{ class: "plan-list" },
|
|
243
|
+
plan.members.map((m) =>
|
|
244
|
+
el("li", null, [
|
|
245
|
+
el("span", { text: `${m.specName} (${m.target})` }),
|
|
246
|
+
m.declaredOrder === null
|
|
247
|
+
? el("span", { class: "muted", text: " no declared order" })
|
|
248
|
+
: null,
|
|
249
|
+
m.skip === null ? null : el("span", { class: "muted", text: ` — skipped: ${m.skip}` }),
|
|
250
|
+
]),
|
|
251
|
+
),
|
|
252
|
+
),
|
|
253
|
+
);
|
|
254
|
+
const run = el("button", {
|
|
255
|
+
class: "btn btn-primary",
|
|
256
|
+
text: `run ${verb} on ${plan.actionable} member(s)`,
|
|
257
|
+
onclick: async () => {
|
|
258
|
+
run.disabled = true;
|
|
259
|
+
run.textContent = `${verb}ing…`;
|
|
260
|
+
try {
|
|
261
|
+
const result = (await api.groupProc(name, verb))?.result ?? null;
|
|
262
|
+
toast(
|
|
263
|
+
result === null
|
|
264
|
+
? `${verb} ${name}: no result`
|
|
265
|
+
: `${verb} ${name}: ${result.ok} ok, ${result.failed} failed${
|
|
266
|
+
result.skipped > 0 ? `, ${result.skipped} skipped` : ""
|
|
267
|
+
}`,
|
|
268
|
+
result !== null && result.failed === 0 ? "ok" : "error",
|
|
269
|
+
);
|
|
270
|
+
reload();
|
|
271
|
+
} catch (err) {
|
|
272
|
+
toast(String(err?.message ?? err), "error");
|
|
273
|
+
run.disabled = false;
|
|
274
|
+
run.textContent = `run ${verb} on ${plan.actionable} member(s)`;
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
body.appendChild(run);
|
|
279
|
+
}
|
|
280
|
+
|
|
161
281
|
function boardRow(feedRow, proc, nowMs, reload) {
|
|
162
282
|
const name = feedRow.specName || dirTail(feedRow.dir, 1) || feedRow.id;
|
|
163
283
|
// The fleet feed's `supervision` is the instant answer; `/proc` refines it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/hangar-ui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Hangar web console's static UI — zero-build, dependency-free browser ES modules embedded as text assets for the single-binary CLI to hand to the hangar server",
|
|
6
6
|
"main": "dist/index.js",
|