@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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HM-11 — the health score, rendered the only way it is allowed to be:
|
|
3
|
+
* with its deductions.
|
|
4
|
+
*
|
|
5
|
+
* A bare "72" is a mystery an operator cannot act on, so the number never
|
|
6
|
+
* appears without the list that produced it, and every row in that list is
|
|
7
|
+
* a LINK to the tab that fixes it. Where the server could not read a signal
|
|
8
|
+
* at all, the card says so — an unknown is not a pass.
|
|
9
|
+
*
|
|
10
|
+
* Two surfaces, one renderer: the per-harness card (Overview) and the fleet
|
|
11
|
+
* board (`#/health`), which is the "needs attention" screen — worst first.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { api } from "../api.js";
|
|
15
|
+
import { clear, collapsible, dot, el, emptyState, skeleton } from "../dom.js";
|
|
16
|
+
import { hrefHarness } from "../router.js";
|
|
17
|
+
|
|
18
|
+
/** band → the traffic-light state, always paired with the text below. */
|
|
19
|
+
const BAND_DOT = { good: "ok", fair: "warn", poor: "bad" };
|
|
20
|
+
const BAND_TEXT = {
|
|
21
|
+
good: "healthy",
|
|
22
|
+
fair: "needs attention",
|
|
23
|
+
poor: "unhealthy",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** One deduction row: the points, the label, the detail, and the fix. */
|
|
27
|
+
function deductionRow(id, deduction) {
|
|
28
|
+
const points = Number(deduction?.points ?? 0);
|
|
29
|
+
const screen = String(deduction?.screen ?? "overview");
|
|
30
|
+
return el("li", { class: "ded-row" }, [
|
|
31
|
+
el("span", { class: "ded-points mono", text: `−${points}` }),
|
|
32
|
+
el("span", { class: "ded-label", text: String(deduction?.label ?? "") }),
|
|
33
|
+
el("span", { class: "ded-detail muted", text: String(deduction?.detail ?? "") }),
|
|
34
|
+
// The whole point of the item: a deduction that names where it is fixed.
|
|
35
|
+
id
|
|
36
|
+
? el("a", { class: "ded-fix", href: hrefHarness(id, screen), text: `fix in ${screen} →` })
|
|
37
|
+
: el("span", { class: "ded-fix muted", text: `fix in ${screen}` }),
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The score card.
|
|
43
|
+
*
|
|
44
|
+
* `id` may be null (a card with no harness behind it renders the fix
|
|
45
|
+
* targets as text rather than links); `name`, when given, puts the harness's
|
|
46
|
+
* name at the top — which is what turns the same card into a fleet-board
|
|
47
|
+
* row.
|
|
48
|
+
*/
|
|
49
|
+
export function healthScoreCard(health, id, name = null) {
|
|
50
|
+
if (!health || typeof health !== "object") {
|
|
51
|
+
return el("section", { class: "card ov-card" }, [
|
|
52
|
+
el("h3", { class: "card-title", text: "Health score" }),
|
|
53
|
+
emptyState("No score yet — the harness could not be read", "crewhaus doctor"),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
const band = String(health.band ?? "poor");
|
|
57
|
+
const deductions = Array.isArray(health.deductions) ? health.deductions : [];
|
|
58
|
+
const unknowns = Array.isArray(health.unknowns) ? health.unknowns : [];
|
|
59
|
+
const body = [
|
|
60
|
+
name !== null
|
|
61
|
+
? el("div", { class: "health-row-head" }, [
|
|
62
|
+
el("a", {
|
|
63
|
+
class: "health-name",
|
|
64
|
+
href: id ? hrefHarness(id) : "#/",
|
|
65
|
+
text: String(name),
|
|
66
|
+
}),
|
|
67
|
+
])
|
|
68
|
+
: null,
|
|
69
|
+
el("div", { class: "health-head" }, [
|
|
70
|
+
el("span", { class: `health-score health-${band}`, text: String(health.score ?? "—") }),
|
|
71
|
+
el("span", { class: "health-of muted", text: "/ 100" }),
|
|
72
|
+
dot(BAND_DOT[band] ?? "unknown", BAND_TEXT[band] ?? band),
|
|
73
|
+
]),
|
|
74
|
+
el("p", { class: "muted", text: String(health.summary ?? "") }),
|
|
75
|
+
];
|
|
76
|
+
if (deductions.length > 0) {
|
|
77
|
+
body.push(
|
|
78
|
+
el(
|
|
79
|
+
"ul",
|
|
80
|
+
{ class: "ded-list", "aria-label": "deductions" },
|
|
81
|
+
deductions.map((d) => deductionRow(id, d)),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (unknowns.length > 0) {
|
|
86
|
+
body.push(
|
|
87
|
+
collapsible(
|
|
88
|
+
[dot("unknown", `${unknowns.length} signal(s) could not be read`)],
|
|
89
|
+
el(
|
|
90
|
+
"ul",
|
|
91
|
+
{ class: "check-list" },
|
|
92
|
+
unknowns.map((u) => el("li", { class: "muted", text: String(u) })),
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return el("section", { class: "card ov-card health-card" }, [
|
|
98
|
+
el("h3", { class: "card-title", text: "Health score" }),
|
|
99
|
+
...body,
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The per-harness card, fetched on its own so a slow preflight never holds
|
|
104
|
+
* the Overview's first paint. */
|
|
105
|
+
export async function renderHealthCard(root, ctx) {
|
|
106
|
+
clear(root).appendChild(skeleton(3));
|
|
107
|
+
let payload = null;
|
|
108
|
+
try {
|
|
109
|
+
payload = await api.health(ctx.id);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
clear(root).appendChild(
|
|
112
|
+
el("section", { class: "card ov-card" }, [
|
|
113
|
+
el("h3", { class: "card-title", text: "Health score" }),
|
|
114
|
+
el("p", { class: "muted", text: err instanceof Error ? err.message : String(err) }),
|
|
115
|
+
]),
|
|
116
|
+
);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
clear(root).appendChild(healthScoreCard(payload?.health ?? null, ctx.id));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** `#/health` — the fleet board, worst first. */
|
|
123
|
+
export async function renderHealthBoard(root) {
|
|
124
|
+
clear(root).appendChild(skeleton(6));
|
|
125
|
+
let payload = null;
|
|
126
|
+
try {
|
|
127
|
+
payload = await api.fleetHealth();
|
|
128
|
+
} catch (err) {
|
|
129
|
+
clear(root).appendChild(
|
|
130
|
+
el("div", { class: "card error-card" }, [
|
|
131
|
+
el("h2", { text: "Fleet health unavailable" }),
|
|
132
|
+
el("p", { class: "muted", text: err instanceof Error ? err.message : String(err) }),
|
|
133
|
+
]),
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const rows = rankHealthRows(payload);
|
|
138
|
+
clear(root);
|
|
139
|
+
root.appendChild(
|
|
140
|
+
el("div", { class: "rollup" }, [
|
|
141
|
+
el("h2", { text: "Fleet health" }),
|
|
142
|
+
el("span", {
|
|
143
|
+
class: "muted",
|
|
144
|
+
text: "every score lists the deductions that produced it — worst first",
|
|
145
|
+
}),
|
|
146
|
+
]),
|
|
147
|
+
);
|
|
148
|
+
if (rows.length === 0) {
|
|
149
|
+
root.appendChild(
|
|
150
|
+
emptyState("No registered harness could be scored yet", "crewhaus harness add <dir>"),
|
|
151
|
+
);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const grid = el("div", { class: "ov-grid" });
|
|
155
|
+
for (const row of rows) {
|
|
156
|
+
grid.appendChild(healthScoreCard(row.health, row.id, row.specName));
|
|
157
|
+
}
|
|
158
|
+
root.appendChild(grid);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Payload → rows, worst score first, ties broken by name so the board does
|
|
163
|
+
* not reshuffle between polls. Pure (unit-tested).
|
|
164
|
+
*/
|
|
165
|
+
export function rankHealthRows(payload) {
|
|
166
|
+
const body = payload && typeof payload === "object" ? payload : {};
|
|
167
|
+
const rows = Array.isArray(body.harnesses) ? body.harnesses : [];
|
|
168
|
+
return rows
|
|
169
|
+
.map((row) => ({
|
|
170
|
+
id: String(row?.id ?? ""),
|
|
171
|
+
specName: String(row?.specName ?? row?.id ?? ""),
|
|
172
|
+
health: row?.health ?? null,
|
|
173
|
+
score: typeof row?.health?.score === "number" ? row.health.score : 100,
|
|
174
|
+
}))
|
|
175
|
+
.sort((a, b) => a.score - b.score || a.specName.localeCompare(b.specName));
|
|
176
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared triage shell both fleet inboxes wear: a selected row, j/k
|
|
3
|
+
* movement, one-key verdicts, a `.` row menu and a `?` bindings sheet.
|
|
4
|
+
*
|
|
5
|
+
* All of the thinking is in `triageKey` — a pure reducer this package
|
|
6
|
+
* unit-tests without a browser. This module only turns its `effect` into an
|
|
7
|
+
* API call and its `state` into a redraw, which is why the bindings can be
|
|
8
|
+
* proven correct without driving a DOM.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { clear, el } from "../dom.js";
|
|
12
|
+
import { isTriageKey, keyHelp, triageKey } from "../supervision.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mount a keyboard-triaged list into `host`.
|
|
16
|
+
*
|
|
17
|
+
* opts:
|
|
18
|
+
* keys — action-key map (`{ g: "grant" }`), also the row menu's contents
|
|
19
|
+
* render — (row, index, state, redraw, reload) → node
|
|
20
|
+
* perform — (row, action, reload) → void
|
|
21
|
+
* reload — refetch + redraw the whole inbox after a decision
|
|
22
|
+
*/
|
|
23
|
+
export function mountTriage(host, rows, opts) {
|
|
24
|
+
const listNode = el("div", { class: "inbox" });
|
|
25
|
+
const helpNode = el("div", { class: "card keyhelp" });
|
|
26
|
+
const menuNode = el("div", { class: "card rowmenu" });
|
|
27
|
+
const reload = opts.reload ?? (() => {});
|
|
28
|
+
let state = { index: 0, count: rows.length, menuOpen: false, helpOpen: false };
|
|
29
|
+
|
|
30
|
+
const redraw = (next) => {
|
|
31
|
+
state = next;
|
|
32
|
+
clear(listNode);
|
|
33
|
+
rows.forEach((row, i) => {
|
|
34
|
+
listNode.appendChild(opts.render(row, i, state, redraw, reload));
|
|
35
|
+
});
|
|
36
|
+
helpNode.hidden = !state.helpOpen;
|
|
37
|
+
menuNode.hidden = !state.menuOpen;
|
|
38
|
+
if (state.menuOpen) {
|
|
39
|
+
const row = rows[state.index];
|
|
40
|
+
clear(menuNode).appendChild(
|
|
41
|
+
el(
|
|
42
|
+
"div",
|
|
43
|
+
{ class: "rowmenu-body" },
|
|
44
|
+
Object.entries(opts.keys).map(([key, action]) =>
|
|
45
|
+
el("button", {
|
|
46
|
+
class: "btn",
|
|
47
|
+
type: "button",
|
|
48
|
+
text: `${action} (${key})`,
|
|
49
|
+
onClick: () => {
|
|
50
|
+
if (row !== undefined) opts.perform(row, action, reload);
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
helpNode.appendChild(
|
|
60
|
+
el(
|
|
61
|
+
"ul",
|
|
62
|
+
{ class: "check-list" },
|
|
63
|
+
keyHelp(opts.keys).map((b) =>
|
|
64
|
+
el("li", null, [
|
|
65
|
+
el("kbd", { class: "mono", text: b.key }),
|
|
66
|
+
el("span", { text: ` — ${b.does}` }),
|
|
67
|
+
]),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const onKey = (event) => {
|
|
73
|
+
if (!listNode.isConnected) {
|
|
74
|
+
document.removeEventListener("keydown", onKey);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const target = event.target;
|
|
78
|
+
// Never steal keys from a field the operator is typing in.
|
|
79
|
+
if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA")) return;
|
|
80
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
81
|
+
if (!isTriageKey(event.key, opts.keys)) return;
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
const { state: next, effect } = triageKey(state, event.key, opts.keys);
|
|
84
|
+
if (effect !== null) {
|
|
85
|
+
const row = rows[effect.index];
|
|
86
|
+
if (row !== undefined) opts.perform(row, effect.action, reload);
|
|
87
|
+
}
|
|
88
|
+
redraw(next);
|
|
89
|
+
};
|
|
90
|
+
document.addEventListener("keydown", onKey);
|
|
91
|
+
|
|
92
|
+
host.appendChild(
|
|
93
|
+
el("div", { class: "inbox-hint muted" }, [
|
|
94
|
+
el("span", { text: "j/k move · " }),
|
|
95
|
+
el("span", { text: `${Object.keys(opts.keys).join("/")} decide · ` }),
|
|
96
|
+
el("span", { text: ". row menu · ? bindings" }),
|
|
97
|
+
]),
|
|
98
|
+
);
|
|
99
|
+
host.appendChild(helpNode);
|
|
100
|
+
host.appendChild(menuNode);
|
|
101
|
+
host.appendChild(listNode);
|
|
102
|
+
redraw(state);
|
|
103
|
+
}
|