@aria-framework/ai 0.15.0 → 0.17.1

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.
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Reordering the endpoints that serve one job.
3
+ *
4
+ * ── WHAT IS BEING EDITED ────────────────────────────────────────────────────────────────────────
5
+ * A path: `a → b → c`. The first endpoint that answers serves the job; the rest are its fallbacks,
6
+ * tried in order. So the ONLY thing that matters here is sequence, and the widget is a list you can
7
+ * move things up and down in — not a grid of checkboxes with a number beside each, which is what
8
+ * this replaced and which made you compute the order in your head before typing it in.
9
+ *
10
+ * ── TWO WAYS TO MOVE, ON PURPOSE ────────────────────────────────────────────────────────────────
11
+ * Drag for a mouse, and ↑/↓ buttons for everything else. The buttons are not a fallback for when
12
+ * drag fails; they are the accessible path and the precise one. Drag-only reordering is unusable
13
+ * from a keyboard and awkward on a trackpad for a list of three, which is the size this list
14
+ * actually is.
15
+ *
16
+ * ── THE ORDER IS RENUMBERED FROM THE DOM, ONCE, ON SUBMIT ───────────────────────────────────────
17
+ * Positions are typed into hidden fields because a form posts in DOCUMENT order and the handler
18
+ * reads `position_<id>` — a list that relied on serialisation order would be silently
19
+ * un-reorderable. Rather than keep those fields in step through every move, add and remove, they
20
+ * are rebuilt from the list itself at the moment of submitting. One place to be right, and it is
21
+ * the place where being wrong would matter.
22
+ *
23
+ * ── NOTHING CHOSEN IS A CONFIGURATION ───────────────────────────────────────────────────────────
24
+ * An empty path means "every enabled endpoint, in listed order", which is a working default and not
25
+ * a missing setting. The note saying so appears exactly when the list empties, because that is the
26
+ * moment somebody would otherwise think they had broken it.
27
+ */
28
+ (function () {
29
+ 'use strict';
30
+
31
+ function card(node) { return node.closest('[data-job]'); }
32
+ function listOf(c) { return c.querySelector('[data-job-list]'); }
33
+
34
+ /** One element, with its text set as TEXT. There is no path here that builds markup from a value. */
35
+ function el(tag, cls, text, attrs) {
36
+ var n = document.createElement(tag);
37
+ if (cls) n.className = cls;
38
+ if (text) n.textContent = text;
39
+ if (attrs) Object.keys(attrs).forEach(function (k) { n.setAttribute(k, attrs[k]); });
40
+ return n;
41
+ }
42
+
43
+ /** Renumber the visible ranks, and show the default note exactly when the path is empty. */
44
+ function refresh(c) {
45
+ var list = listOf(c);
46
+ if (!list) return;
47
+ var items = list.querySelectorAll('[data-job-item]');
48
+ for (var i = 0; i < items.length; i += 1) {
49
+ var rank = items[i].querySelector('[data-job-rank]');
50
+ if (rank) rank.textContent = String(i + 1);
51
+ }
52
+ var note = c.querySelector('[data-job-empty]');
53
+ if (note) note.hidden = items.length > 0;
54
+ }
55
+
56
+ function setMode(c, editing) {
57
+ var view = c.querySelector('[data-job-view]');
58
+ var form = c.querySelector('[data-job-edit]');
59
+ if (!view || !form) return;
60
+ view.hidden = editing;
61
+ form.hidden = !editing;
62
+ if (editing) {
63
+ refresh(c);
64
+ var first = form.querySelector('[data-job-up], [data-job-add], button[type="submit"]');
65
+ if (first) first.focus();
66
+ }
67
+ }
68
+
69
+ /** Put an endpoint back in the pool, in a stable place rather than wherever it was removed from. */
70
+ function toPool(c, id) {
71
+ var pool = c.querySelector('[data-job-pool]');
72
+ if (!pool) return;
73
+ var existing = pool.querySelector('[data-job-add="' + id + '"]');
74
+ if (existing) { existing.hidden = false; return; }
75
+ var b = document.createElement('button');
76
+ b.type = 'button';
77
+ b.className = 'btn btn-sm btn-outline-secondary font-monospace';
78
+ b.setAttribute('data-job-add', id);
79
+ b.textContent = '+ ' + id;
80
+ pool.appendChild(b);
81
+ }
82
+
83
+ document.addEventListener('click', function (ev) {
84
+ var open = ev.target.closest('[data-job-edit-open]');
85
+ if (open) { setMode(card(open), true); return; }
86
+
87
+ var cancel = ev.target.closest('[data-job-cancel]');
88
+ if (cancel) {
89
+ // A CANCEL THAT LEFT THE LIST REARRANGED would be a lie. Reloading is blunt but it is the
90
+ // only thing that is certainly correct, and this is not a hot path.
91
+ window.location.reload();
92
+ return;
93
+ }
94
+
95
+ var up = ev.target.closest('[data-job-up]');
96
+ var down = ev.target.closest('[data-job-down]');
97
+ if (up || down) {
98
+ var item = (up || down).closest('[data-job-item]');
99
+ var c = card(item);
100
+ if (up && item.previousElementSibling) {
101
+ item.parentNode.insertBefore(item, item.previousElementSibling);
102
+ } else if (down && item.nextElementSibling) {
103
+ item.parentNode.insertBefore(item.nextElementSibling, item);
104
+ }
105
+ refresh(c);
106
+ // KEEP THE FOCUS ON THE BUTTON THAT MOVED. Without this a keyboard user presses ↓, focus
107
+ // returns to the top of the document, and the list can only be reordered one step per visit.
108
+ (up || down).focus();
109
+ return;
110
+ }
111
+
112
+ var remove = ev.target.closest('[data-job-remove]');
113
+ if (remove) {
114
+ var row = remove.closest('[data-job-item]');
115
+ var c2 = card(row);
116
+ var id = row.getAttribute('data-job-item');
117
+ row.parentNode.removeChild(row);
118
+ toPool(c2, id);
119
+ refresh(c2);
120
+ return;
121
+ }
122
+
123
+ var add = ev.target.closest('[data-job-add]');
124
+ if (add) {
125
+ var c3 = card(add);
126
+ var list = listOf(c3);
127
+ if (!list) return;
128
+ var addId = add.getAttribute('data-job-add');
129
+ // BUILT AS NODES, never as a markup string. The id is a value an operator typed, and this is
130
+ // the one place on this screen it would be assembled into HTML — so it is not assembled into
131
+ // HTML at all. Every string here reaches the page through textContent.
132
+ var li = document.createElement('li');
133
+ li.className = 'd-flex align-items-center gap-2 border rounded px-2 py-2 mb-1 bg-body-tertiary';
134
+ li.setAttribute('draggable', 'true');
135
+ li.setAttribute('data-job-item', addId);
136
+
137
+ li.appendChild(el('span', 'text-body-tertiary', '⠿', { 'aria-hidden': 'true', style: 'cursor:grab' }));
138
+ li.appendChild(el('span',
139
+ 'badge rounded-pill border bg-body-secondary text-body-secondary font-monospace fw-normal',
140
+ '', { 'data-job-rank': '' }));
141
+ li.appendChild(el('span', 'flex-grow-1 font-monospace small', addId));
142
+
143
+ var group = el('span', 'btn-group btn-group-sm', '', { role: 'group' });
144
+ [['data-job-up', '↑', 'Move ' + addId + ' earlier'],
145
+ ['data-job-down', '↓', 'Move ' + addId + ' later'],
146
+ ['data-job-remove', '✕', 'Remove ' + addId + ' from this job']].forEach(function (b) {
147
+ var btn = el('button', 'btn btn-outline-secondary', b[1], { type: 'button', 'aria-label': b[2] });
148
+ btn.setAttribute(b[0], '');
149
+ group.appendChild(btn);
150
+ });
151
+ li.appendChild(group);
152
+ // ADDED AT THE END, because a new fallback goes behind the ones already trusted. Adding at
153
+ // the front would silently repoint the job at a machine nobody asked to promote.
154
+ list.appendChild(li);
155
+ add.hidden = true;
156
+ refresh(c3);
157
+ return;
158
+ }
159
+ });
160
+
161
+ // ── drag, for a mouse ─────────────────────────────────────────────────────────────────────────
162
+ var dragging = null;
163
+
164
+ document.addEventListener('dragstart', function (ev) {
165
+ var item = ev.target.closest && ev.target.closest('[data-job-item]');
166
+ if (!item) return;
167
+ dragging = item;
168
+ item.style.opacity = '.5';
169
+ if (ev.dataTransfer) {
170
+ ev.dataTransfer.effectAllowed = 'move';
171
+ // Firefox will not start a drag at all without data set on the transfer.
172
+ try { ev.dataTransfer.setData('text/plain', item.getAttribute('data-job-item')); } catch (e) { /* older browsers */ }
173
+ }
174
+ });
175
+
176
+ document.addEventListener('dragend', function () {
177
+ if (!dragging) return;
178
+ dragging.style.opacity = '';
179
+ var c = card(dragging);
180
+ dragging = null;
181
+ if (c) refresh(c);
182
+ });
183
+
184
+ document.addEventListener('dragover', function (ev) {
185
+ if (!dragging) return;
186
+ var over = ev.target.closest && ev.target.closest('[data-job-item]');
187
+ if (!over || over === dragging) return;
188
+ // Only within the SAME job. Two jobs are two independent paths, and dragging between them would
189
+ // mean something nobody asked for.
190
+ if (card(over) !== card(dragging)) return;
191
+ ev.preventDefault();
192
+ var rect = over.getBoundingClientRect();
193
+ var after = (ev.clientY - rect.top) > rect.height / 2;
194
+ over.parentNode.insertBefore(dragging, after ? over.nextSibling : over);
195
+ });
196
+
197
+ document.addEventListener('drop', function (ev) { if (dragging) ev.preventDefault(); });
198
+
199
+ // ── submit: the DOM order becomes the posted order ────────────────────────────────────────────
200
+ document.addEventListener('submit', function (ev) {
201
+ var form = ev.target;
202
+ if (!form || !form.hasAttribute || !form.hasAttribute('data-job-edit')) return;
203
+ var c = card(form);
204
+ var list = listOf(c);
205
+ if (!list) return;
206
+
207
+ // Rebuild rather than reconcile: every previous field goes, and the list writes its own.
208
+ var old = form.querySelectorAll('[data-job-field]');
209
+ for (var i = 0; i < old.length; i += 1) old[i].parentNode.removeChild(old[i]);
210
+
211
+ var items = list.querySelectorAll('[data-job-item]');
212
+ for (var j = 0; j < items.length; j += 1) {
213
+ var id = items[j].getAttribute('data-job-item');
214
+ form.appendChild(hidden('endpoint', id));
215
+ form.appendChild(hidden('position_' + id, String(j)));
216
+ }
217
+ });
218
+
219
+ function hidden(name, value) {
220
+ var i = document.createElement('input');
221
+ i.type = 'hidden';
222
+ i.name = name;
223
+ i.value = value;
224
+ i.setAttribute('data-job-field', '');
225
+ return i;
226
+ }
227
+ })();
@@ -1,113 +1,138 @@
1
- /**
2
- * The fold on the Inference list, and prefilling the supervisor form from a panel.
3
- *
4
- * WHY ANYTHING FOLDS. A stable stack is four engines, three credentials, a certificate fingerprint
5
- * and a verification report — worth having, not worth reading every time you open this page to do
6
- * something else. Folded, a panel is one line that still carries the whole verdict: status, the
7
- * count of what depends on it, all three credentials and when it was last checked. Folding hides
8
- * detail; it must never hide the reason you would have opened it.
9
- *
10
- * A PANEL WITH A PROBLEM IGNORES WHAT YOU REMEMBERED. `data-panel-attention` is stamped by the
11
- * server on anything with a missing engine, an expiring certificate, a failed check or a nearly
12
- * spent cap. Those open and stay open. Attention beats tidiness — the alternative is a page that
13
- * quietly honours a fold you chose last week and hides the thing that broke yesterday.
14
- *
15
- * THE PREFERENCE IS PER BROWSER AND DISPOSABLE. It is a convenience about how a page looks to one
16
- * person, so localStorage is the right home and losing it costs nothing. Every access is guarded:
17
- * a browser set to block site data throws on read, and a settings page must not break because
18
- * somebody tightened their privacy settings.
19
- */
20
- (function () {
21
- 'use strict';
22
-
23
- var KEY = 's101.ai.panels';
24
-
25
- function readPrefs() {
26
- try {
27
- return JSON.parse(window.localStorage.getItem(KEY) || '{}') || {};
28
- } catch (e) {
29
- return {};
30
- }
31
- }
32
-
33
- function writePref(id, open) {
34
- try {
35
- var prefs = readPrefs();
36
- prefs[id] = !!open;
37
- window.localStorage.setItem(KEY, JSON.stringify(prefs));
38
- } catch (e) { /* private window, or site data blocked — the fold still works for this visit */ }
39
- }
40
-
41
- function bodyOf(panel) {
42
- var btn = panel.querySelector('[data-panel-toggle]');
43
- if (!btn) return null;
44
- return document.getElementById(btn.getAttribute('aria-controls'));
45
- }
46
-
47
- function setOpen(panel, open, remember) {
48
- var btn = panel.querySelector('[data-panel-toggle]');
49
- var body = bodyOf(panel);
50
- if (!btn || !body) return;
51
- // `hidden`, not a style: the server renders the closed state the same way, so a panel does not
52
- // flicker open on load before this script runs.
53
- body.hidden = !open;
54
- btn.setAttribute('aria-expanded', open ? 'true' : 'false');
55
- panel.classList.toggle('panel-open', open);
56
- if (remember) writePref(panel.getAttribute('data-panel'), open);
57
- }
58
-
59
- function restore() {
60
- var prefs = readPrefs();
61
- var panels = document.querySelectorAll('[data-panel]');
62
- for (var i = 0; i < panels.length; i += 1) {
63
- var panel = panels[i];
64
- var id = panel.getAttribute('data-panel');
65
- // The server already opened this one and means it. Do not consult the preference at all —
66
- // reading it and then ignoring it is the same thing, but invites somebody to "fix" it later.
67
- if (panel.hasAttribute('data-panel-attention')) {
68
- setOpen(panel, true, false);
69
- continue;
70
- }
71
- if (Object.prototype.hasOwnProperty.call(prefs, id)) setOpen(panel, !!prefs[id], false);
72
- }
73
- }
74
-
75
- document.addEventListener('click', function (ev) {
76
- var toggle = ev.target.closest('[data-panel-toggle]');
77
- if (toggle) {
78
- var panel = toggle.closest('[data-panel]');
79
- if (!panel) return;
80
- var body = bodyOf(panel);
81
- setOpen(panel, !!(body && body.hidden), true);
82
- return;
83
- }
84
-
85
- // EDIT PREFILLS FROM THE PANEL, so changing a stack is not retyping it.
86
- //
87
- // The two secrets stay BLANK on purpose. Blank means keep, which is the rule the handler
88
- // already follows, and it is why correcting an address cannot cost you the credentials. There
89
- // is no read-back to prefill them with in any case: they are write-only by design.
90
- var edit = ev.target.closest('[data-lmx-edit]');
91
- if (!edit) return;
92
- var src = document.querySelector('[data-lmx-id="' + edit.getAttribute('data-lmx-edit') + '"]');
93
- var form = document.querySelector('#lmx-new form');
94
- if (!src || !form) return;
95
- var set = function (name, value) {
96
- var field = form.querySelector('[name="' + name + '"]');
97
- if (field) field.value = value == null ? '' : value;
98
- };
99
- set('id', src.getAttribute('data-lmx-id'));
100
- set('label', src.getAttribute('data-lmx-label'));
101
- set('status_url', src.getAttribute('data-lmx-url'));
102
- set('status_token', '');
103
- set('engines_key', '');
104
- var heading = document.querySelector('#lmx-new .card-title');
105
- if (heading) heading.textContent = 'Change ' + (src.getAttribute('data-lmx-label') || src.getAttribute('data-lmx-id'));
106
- });
107
-
108
- if (document.readyState === 'loading') {
109
- document.addEventListener('DOMContentLoaded', restore);
110
- } else {
111
- restore();
112
- }
113
- })();
1
+ /**
2
+ * The fold on the Inference list, and prefilling the supervisor form from a panel.
3
+ *
4
+ * WHY ANYTHING FOLDS. A stable stack is four engines, three credentials, a certificate fingerprint
5
+ * and a verification report — worth having, not worth reading every time you open this page to do
6
+ * something else. Folded, a panel is one line that still carries the whole verdict: status, the
7
+ * count of what depends on it, all three credentials and when it was last checked. Folding hides
8
+ * detail; it must never hide the reason you would have opened it.
9
+ *
10
+ * A PANEL WITH A PROBLEM IGNORES WHAT YOU REMEMBERED. `data-panel-attention` is stamped by the
11
+ * server on anything with a missing engine, an expiring certificate, a failed check or a nearly
12
+ * spent cap. Those open and stay open. Attention beats tidiness — the alternative is a page that
13
+ * quietly honours a fold you chose last week and hides the thing that broke yesterday.
14
+ *
15
+ * THE PREFERENCE IS PER BROWSER AND DISPOSABLE. It is a convenience about how a page looks to one
16
+ * person, so localStorage is the right home and losing it costs nothing. Every access is guarded:
17
+ * a browser set to block site data throws on read, and a settings page must not break because
18
+ * somebody tightened their privacy settings.
19
+ */
20
+ (function () {
21
+ 'use strict';
22
+
23
+ var KEY = 's101.ai.panels';
24
+
25
+ function readPrefs() {
26
+ try {
27
+ return JSON.parse(window.localStorage.getItem(KEY) || '{}') || {};
28
+ } catch (e) {
29
+ return {};
30
+ }
31
+ }
32
+
33
+ function writePref(id, open) {
34
+ try {
35
+ var prefs = readPrefs();
36
+ prefs[id] = !!open;
37
+ window.localStorage.setItem(KEY, JSON.stringify(prefs));
38
+ } catch (e) { /* private window, or site data blocked — the fold still works for this visit */ }
39
+ }
40
+
41
+ function bodyOf(panel) {
42
+ var btn = panel.querySelector('[data-panel-toggle]');
43
+ if (!btn) return null;
44
+ return document.getElementById(btn.getAttribute('aria-controls'));
45
+ }
46
+
47
+ function setOpen(panel, open, remember) {
48
+ var btn = panel.querySelector('[data-panel-toggle]');
49
+ var body = bodyOf(panel);
50
+ if (!btn || !body) return;
51
+ // `hidden`, not a style: the server renders the closed state the same way, so a panel does not
52
+ // flicker open on load before this script runs.
53
+ body.hidden = !open;
54
+ btn.setAttribute('aria-expanded', open ? 'true' : 'false');
55
+ panel.classList.toggle('panel-open', open);
56
+ if (remember) writePref(panel.getAttribute('data-panel'), open);
57
+ }
58
+
59
+ function restore() {
60
+ var prefs = readPrefs();
61
+ var panels = document.querySelectorAll('[data-panel]');
62
+ for (var i = 0; i < panels.length; i += 1) {
63
+ var panel = panels[i];
64
+ var id = panel.getAttribute('data-panel');
65
+ // The server already opened this one and means it. Do not consult the preference at all —
66
+ // reading it and then ignoring it is the same thing, but invites somebody to "fix" it later.
67
+ if (panel.hasAttribute('data-panel-attention')) {
68
+ setOpen(panel, true, false);
69
+ continue;
70
+ }
71
+ if (Object.prototype.hasOwnProperty.call(prefs, id)) setOpen(panel, !!prefs[id], false);
72
+ }
73
+ }
74
+
75
+ document.addEventListener('click', function (ev) {
76
+ var toggle = ev.target.closest('[data-panel-toggle]');
77
+ if (toggle) {
78
+ var panel = toggle.closest('[data-panel]');
79
+ if (!panel) return;
80
+ var body = bodyOf(panel);
81
+ setOpen(panel, !!(body && body.hidden), true);
82
+ return;
83
+ }
84
+
85
+ // ── EDITING A STACK HAPPENS IN THE PANEL ────────────────────────────────────────────────────
86
+ //
87
+ // It used to happen in a form at the foot of the page, and Edit scrolled you down to it — away
88
+ // from the stack you were reading, into a form that also served as "Add a supervisor" and said
89
+ // nothing about which stack it had been filled with. Nothing needs prefilling now: every panel
90
+ // renders its own form, from the server, with its own values already in it.
91
+ var open = ev.target.closest('[data-stack-edit-open]');
92
+ if (open) { stackMode(open.closest('[data-stack]'), true); return; }
93
+
94
+ var cancel = ev.target.closest('[data-stack-cancel]');
95
+ if (cancel) {
96
+ var panel2 = cancel.closest('[data-stack]');
97
+ // A NEW stack has no reading half to go back to, so cancelling it puts the blank panel away.
98
+ if (panel2 && panel2.hasAttribute('data-stack-new')) { panel2.hidden = true; return; }
99
+ // Otherwise reload rather than restore: a Cancel that left half-typed values behind, ready to
100
+ // be posted by the next Save, would be worse than the extra request.
101
+ window.location.reload();
102
+ return;
103
+ }
104
+
105
+ // "Add LMX" reveals the blank panel that is already on the page, at the top of the list.
106
+ var add = ev.target.closest('[data-stack-add]');
107
+ if (add) {
108
+ var blank = document.querySelector('[data-stack-new]');
109
+ if (!blank) return;
110
+ blank.hidden = false;
111
+ blank.scrollIntoView({ block: 'nearest' });
112
+ var id = blank.querySelector('[name="id"]');
113
+ if (id) id.focus();
114
+ }
115
+ });
116
+
117
+ /** Swap one stack panel between reading and editing. */
118
+ function stackMode(panel, editing) {
119
+ if (!panel) return;
120
+ var view = panel.querySelector('[data-stack-view]');
121
+ var form = panel.querySelector('[data-stack-edit]');
122
+ if (!view || !form) return;
123
+ view.hidden = editing;
124
+ form.hidden = !editing;
125
+ if (editing) {
126
+ // The id is readonly on an existing stack, so focus the first field somebody can actually
127
+ // change rather than one that will not accept a keystroke.
128
+ var first = form.querySelector('[name="label"]') || form.querySelector('[name="status_url"]');
129
+ if (first) first.focus();
130
+ }
131
+ }
132
+
133
+ if (document.readyState === 'loading') {
134
+ document.addEventListener('DOMContentLoaded', restore);
135
+ } else {
136
+ restore();
137
+ }
138
+ })();
package/lmxStatus.js CHANGED
@@ -60,7 +60,12 @@ function describeEngine(e) {
60
60
  if (!e) return e;
61
61
  return Object.assign({}, e, {
62
62
  modelName: modelName(e.model),
63
- reasoning: e.reasoning || reasoningLabel(e.model) || null
63
+ // ONLY FOR AN ENGINE THAT REASONS. The flag is matched on the model NAME, and an embedding
64
+ // model is very often the same family as a chat one — `Qwen3-Embedding-0.6B` matches every
65
+ // pattern `qwen3-35b-instruct` does. Labelling it made the panel promise a flag on a call that
66
+ // has no reasoning to configure and would never carry one, which is worse than saying nothing:
67
+ // the whole reason this line exists is that an operator cannot otherwise tell what gets sent.
68
+ reasoning: (e.role && e.role !== 'chat') ? null : (e.reasoning || reasoningLabel(e.model) || null)
64
69
  });
65
70
  }
66
71
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aria-framework/ai",
3
3
  "description": "Aria App Framework \u2014 AI module. A dependency-injected model seam (createAiClient) over several providers (LM Studio / OpenAI-compatible / Anthropic), with a fact-preservation guard, generic Polish and Generate writing engines, and a browser polish widget. Prompts and config stay in the consuming app.",
4
- "version": "0.15.0",
4
+ "version": "0.17.1",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
7
7
  "publishConfig": {
@@ -29,7 +29,8 @@
29
29
  "lmxVerify.js",
30
30
  "lmxStore.js",
31
31
  "lmxStatus.js",
32
- "browser/ai-panels.js"
32
+ "browser/ai-panels.js",
33
+ "browser/ai-jobs.js"
33
34
  ],
34
35
  "peerDependencies": {
35
36
  "@aria-framework/db-worker": ">=0.7.0",
@@ -44,7 +45,7 @@
44
45
  }
45
46
  },
46
47
  "scripts": {
47
- "test": "node test/smoke.js && node test/usageStore.js && node test/providerStore.js && node test/speedStore.js && node test/health.js && node test/listModels.js && node test/benchmark.js && node test/lmxDiscovery.js && node test/lmx.js && node test/lmxVerify.js && node test/lmxStore.js && node test/lmxStatus.js && node test/packaging.js && node test/views.js"
48
+ "test": "node test/smoke.js && node test/usageStore.js && node test/providerStore.js && node test/speedStore.js && node test/health.js && node test/listModels.js && node test/benchmark.js && node test/lmxDiscovery.js && node test/lmx.js && node test/lmxVerify.js && node test/lmxStore.js && node test/lmxStatus.js && node test/jobCard.js && node test/packaging.js && node test/views.js"
48
49
  },
49
50
  "devDependencies": {
50
51
  "undici": "^8.10.0",
@@ -0,0 +1,170 @@
1
+ <%# One job, and the path of endpoints that serves it.
2
+ ────────────────────────────────────────────────────────────────────────────────────────────
3
+ TWO MODES, NOT A FOLD. A stack panel folds because its detail is worth reading occasionally; a
4
+ job's detail is a thing you EDIT, and the editing controls are useless the other 99% of the
5
+ time. So the card reads as a sentence — this job, this path, this is what it is for — until
6
+ somebody says they want to change it.
7
+
8
+ THE PATH IS THE HEADLINE, because it is the only part that varies and the only part anybody
9
+ comes here to check. `a → b → c` is the whole configuration of a job in one line: the first
10
+ that answers serves, the rest are its fallbacks in order.
11
+
12
+ ORDER IS CARRIED IN TYPED POSITIONS, never in the order the browser serialises fields. A form
13
+ posts in document order, so a list that relied on that would be silently un-reorderable — and
14
+ the handler this posts to already reads `position_<id>` for exactly that reason. The list is
15
+ renumbered from its own DOM order on submit, so what you see is what is sent.
16
+
17
+ ASSIGNED IS NOT THE SAME AS RESOLVED. A job with no assignment is not unconfigured, it is
18
+ following the default — every enabled endpoint in listed order — and saying so stops somebody
19
+ "fixing" a job that is already doing the right thing. A job whose assigned endpoints are all
20
+ disabled resolves to nothing at all, which is a different and much worse state.
21
+
22
+ Locals:
23
+ job { id, label, character, detail }
24
+ assigned [endpointId] explicitly chosen for this job, in order (may be empty)
25
+ resolved [endpointId] what would actually serve right now, in order
26
+ endpoints [{ id, label, enabled }] every endpoint that exists
27
+ spend { calls, total_tokens } | null
28
+ canEdit render the edit affordance at all
29
+ csrfToken () => token
30
+ basePath where this app mounts its AI admin, e.g. '/admin/ai'
31
+ %>
32
+ <%
33
+ var _edit = (typeof canEdit !== 'undefined') && canEdit;
34
+ var _base = (typeof basePath !== 'undefined' && basePath) ? basePath : '/admin/ai';
35
+ var _csrf = (typeof csrfToken === 'function') ? csrfToken : function () { return ''; };
36
+ var _assigned = assigned || [];
37
+ var _resolved = resolved || [];
38
+ var _eps = endpoints || [];
39
+ var _spend = (typeof spend !== 'undefined') ? spend : null;
40
+ var _isDefault = !_assigned.length;
41
+ var _byId = {};
42
+ _eps.forEach(function (e) { _byId[e.id] = e; });
43
+ // The path being EDITED is the assignment, not the resolution: editing what is currently serving
44
+ // would silently drop a disabled endpoint from the assignment the moment anybody pressed Save.
45
+ var _path = _assigned.slice();
46
+ var _rest = _eps.filter(function (e) { return _path.indexOf(e.id) === -1; });
47
+ %>
48
+ <div class="border rounded mb-2" data-job="<%= job.id %>">
49
+
50
+ <%# ── READING MODE ────────────────────────────────────────────────────────────────────────── %>
51
+ <div class="p-3" data-job-view>
52
+ <%# THE ACTIONS STAY ON THE RIGHT, whatever the description does.
53
+ `flex-wrap` plus a growing child with no `min-width: 0` is the classic way to lose a
54
+ button: a flex item will not shrink below its content's intrinsic width, so a long
55
+ description keeps the text column at full width, the actions wrap onto a line of their own,
56
+ and `justify-content-between` then puts that lone item at the START — bottom LEFT, which is
57
+ precisely where the Edit button went.
58
+ `min-width: 0` lets the text column shrink so its own text wraps instead; `flex-shrink-0`
59
+ keeps the buttons their natural size; and with no wrapping there is no second line to fall
60
+ onto. %>
61
+ <div class="d-flex justify-content-between align-items-start gap-3">
62
+ <div class="flex-grow-1" style="min-width:0">
63
+ <div class="d-flex align-items-center gap-2 flex-wrap">
64
+ <strong><%= job.label %></strong>
65
+ <span class="badge border bg-body-tertiary text-body-secondary text-uppercase fw-semibold"><%= job.character %></span>
66
+ <% if (_isDefault) { %>
67
+ <span class="badge rounded-pill border bg-body-secondary text-body-secondary fw-semibold">default</span>
68
+ <% } %>
69
+ </div>
70
+ <%# THE PATH, directly under the name — the one line that says how this job is configured. %>
71
+ <div class="font-monospace small mt-1" style="word-break:break-all">
72
+ <% if (_resolved.length) { %>
73
+ <% _resolved.forEach(function (id, i) { %><% if (i) { %><span class="text-body-tertiary"> → </span><% } %><span class="<%= i ? 'text-body-secondary' : '' %>"><%= id %></span><% }) %>
74
+ <% } else { %>
75
+ <span class="text-danger">nothing available — every endpoint for this job is disabled or missing</span>
76
+ <% } %>
77
+ </div>
78
+ <div class="form-text mt-1 mb-0"><%= job.detail %></div>
79
+ </div>
80
+
81
+ <div class="text-end d-flex flex-column align-items-end gap-2 flex-shrink-0">
82
+ <%# WHAT THIS JOB COSTS, which is the number worth acting on: a job can be pointed at a
83
+ cheaper machine, run less often, or switched off. An endpoint total only says where the
84
+ money went, never what asked for it. %>
85
+ <% if (_spend && _spend.total_tokens) { %>
86
+ <div class="small text-body-secondary" style="font-variant-numeric: tabular-nums">
87
+ <%= Number(_spend.total_tokens).toLocaleString() %> tokens in <%= _spend.calls %> call<%= _spend.calls === 1 ? '' : 's' %>
88
+ <span class="text-body-tertiary">· 14 days</span>
89
+ </div>
90
+ <% } %>
91
+ <% if (_edit && _eps.length) { %>
92
+ <button type="button" class="btn btn-sm btn-outline-secondary" data-job-edit-open="<%= job.id %>">Edit</button>
93
+ <% } %>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <%# ── EDITING MODE ───────────────────────────────────────────────────────────────────────────
99
+ Rendered by the server and hidden, rather than built by script when Edit is pressed: the
100
+ whole list is here in the markup, so it works from a keyboard, survives a script that fails
101
+ to load badly enough to matter, and posts the same fields the handler has always read. %>
102
+ <% if (_edit && _eps.length) { %>
103
+ <form method="POST" action="<%= _base %>/routes/<%= encodeURIComponent(job.id) %>"
104
+ class="p-3 border-top" data-job-edit hidden>
105
+ <input type="hidden" name="_csrf" value="<%= _csrf() %>">
106
+
107
+ <div class="d-flex justify-content-between align-items-baseline mb-2">
108
+ <span class="small text-uppercase fw-semibold text-body-secondary" style="letter-spacing:.08em">
109
+ <%= job.label %> — the path, in order
110
+ </span>
111
+ <span class="small text-body-tertiary">first to answer serves</span>
112
+ </div>
113
+
114
+ <ol class="list-unstyled mb-2" data-job-list>
115
+ <% _path.forEach(function (id, i) { %>
116
+ <% var ep = _byId[id]; %>
117
+ <li class="d-flex align-items-center gap-2 border rounded px-2 py-2 mb-1 bg-body-tertiary"
118
+ draggable="true" data-job-item="<%= id %>">
119
+ <span class="text-body-tertiary" aria-hidden="true" style="cursor:grab">⠿</span>
120
+ <span class="badge rounded-pill border bg-body-secondary text-body-secondary font-monospace fw-normal"
121
+ data-job-rank><%= i + 1 %></span>
122
+ <span class="flex-grow-1 font-monospace small">
123
+ <%= id %>
124
+ <% if (ep && ep.label && ep.label !== id) { %><span class="text-body-tertiary"> · <%= ep.label %></span><% } %>
125
+ <%# AN ENDPOINT IN THE PATH BUT DISABLED IS KEPT, and said out loud. Dropping it on
126
+ save would quietly rewrite somebody's configuration because a machine happened to
127
+ be switched off this afternoon. %>
128
+ <% if (ep && !ep.enabled) { %>
129
+ <span class="badge rounded-pill border bg-warning-subtle text-warning-emphasis border-warning-subtle ms-1">disabled — skipped while off</span>
130
+ <% } %>
131
+ <% if (!ep) { %>
132
+ <span class="badge rounded-pill border bg-danger-subtle text-danger-emphasis border-danger-subtle ms-1">no such endpoint</span>
133
+ <% } %>
134
+ </span>
135
+ <span class="btn-group btn-group-sm" role="group">
136
+ <button type="button" class="btn btn-outline-secondary" data-job-up
137
+ aria-label="Move <%= id %> earlier">↑</button>
138
+ <button type="button" class="btn btn-outline-secondary" data-job-down
139
+ aria-label="Move <%= id %> later">↓</button>
140
+ <button type="button" class="btn btn-outline-secondary" data-job-remove
141
+ aria-label="Remove <%= id %> from this job">✕</button>
142
+ </span>
143
+ </li>
144
+ <% }) %>
145
+ </ol>
146
+
147
+ <p class="form-text mt-0" data-job-empty<% if (_path.length) { %> hidden<% } %>>
148
+ Nothing chosen, so this job uses <strong>every enabled endpoint</strong> in the order they
149
+ are listed above. That is a working configuration, not a missing one.
150
+ </p>
151
+
152
+ <% if (_rest.length) { %>
153
+ <div class="small text-uppercase fw-semibold text-body-secondary mt-3 mb-2" style="letter-spacing:.08em">Not in this job</div>
154
+ <div class="d-flex flex-wrap gap-2" data-job-pool>
155
+ <% _rest.forEach(function (e) { %>
156
+ <button type="button" class="btn btn-sm btn-outline-secondary font-monospace"
157
+ data-job-add="<%= e.id %>"<% if (!e.enabled) { %> disabled title="This endpoint is switched off"<% } %>>
158
+ + <%= e.id %><% if (!e.enabled) { %> (disabled)<% } %>
159
+ </button>
160
+ <% }) %>
161
+ </div>
162
+ <% } %>
163
+
164
+ <div class="d-flex gap-2 mt-3">
165
+ <button type="submit" class="btn btn-sm btn-primary">Save</button>
166
+ <button type="button" class="btn btn-sm btn-outline-secondary" data-job-cancel>Cancel</button>
167
+ </div>
168
+ </form>
169
+ <% } %>
170
+ </div>
@@ -18,7 +18,7 @@
18
18
  Locals:
19
19
  inst a supervisor row, merged with lmxStatus.describeStack() and carrying
20
20
  `report` (lmxVerify), `pin` (lmxVerify.readPin), `creds` {statusToken, enginesKey}
21
- _edit render the controls at all
21
+ canEdit render the controls at all
22
22
  csrfToken () => token, for the forms
23
23
  basePath where this app mounts its AI admin, e.g. '/admin/ai'. NOT hardcoded: the routes
24
24
  are the consumer's, and only the consumer knows where they live.
@@ -26,7 +26,7 @@
26
26
  routeLabel optional (id) => human name for a job; the id is used when absent
27
27
  %>
28
28
  <%
29
- // DISTINCT NAMES, deliberately. `var _edit = (typeof canEdit === "undefined") ? ... : _edit`
29
+ // DISTINCT NAMES, deliberately. `var canEdit = (typeof canEdit === "undefined") ? ... : canEdit`
30
30
  // reads as a defaulting idiom and is not one: `var` hoists, so inside this template the name is
31
31
  // already declared and undefined when the typeof runs. The test is therefore always true, the
32
32
  // passed-in local is shadowed, and every consumer silently gets the default.
@@ -44,22 +44,60 @@
44
44
  };
45
45
  var _glyph = { pass: '✔', fail: '✖', warn: '⚠', skip: '—' };
46
46
  var _gcls = { pass: 'text-success', fail: 'text-danger', warn: 'text-warning', skip: 'text-body-tertiary' };
47
+ // ── TONE, NOT COLOUR ────────────────────────────────────────────────────────────────────────
48
+ // A TINT rather than a solid fill. A row can carry four state pills at once, and as saturated
49
+ // blocks they read as traffic lights — at which point nothing looks urgent because everything
50
+ // does. The `-subtle` pairs are Bootstrap's own and are defined for BOTH themes, so these stay
51
+ // legible on either ground; a hand-picked hex would be right on one and wrong on the other.
52
+ var _pill = 'badge rounded-pill border fw-semibold';
53
+ var _tone = {
54
+ ok: 'bg-success-subtle text-success-emphasis border-success-subtle',
55
+ warn: 'bg-warning-subtle text-warning-emphasis border-warning-subtle',
56
+ bad: 'bg-danger-subtle text-danger-emphasis border-danger-subtle',
57
+ mute: 'bg-body-secondary text-body-secondary border'
58
+ };
59
+ // The dot inherits the pill's colour, so state is one decision rather than two that can disagree.
60
+ var _dot = '<span class="d-inline-block rounded-circle align-middle" style="width:.4rem;height:.4rem;background:currentColor"></span>';
61
+ // A COUNT IS NOT A STATE, so it never wears a dot — the status pill beside it owns that job.
62
+ var _count = 'badge rounded-pill border bg-body-secondary text-body-secondary font-monospace fw-normal';
63
+ // What an engine IS, rather than how it is doing: quiet, uppercase, no colour of its own.
64
+ var _role = 'badge border bg-body-tertiary text-body-secondary text-uppercase fw-semibold';
65
+ // The rows for engines actually being relied on, tinted with the app's own brand so the half of
66
+ // the list that matters is visible before a word is read.
67
+ var _inUse = 'background: rgba(var(--bs-primary-rgb, 13,110,253), .06)';
68
+
69
+ // A BLANK PANEL FOR A STACK THAT DOES NOT EXIST YET. Adding and changing are the same form and
70
+ // the same act — the handler already treats an existing id as an update — so they are the same
71
+ // markup, and the only difference is that a new one has no reading half to return to.
72
+ var _isNew = !!inst.isNew;
47
73
  var r = inst.report;
48
74
  var _open = !!inst.attention;
49
75
  // NOT CHECKED is its own state, distinct from both answering and broken. A stack nobody has asked
50
76
  // since the last restart is not a stack in trouble, and colouring it as one would train an
51
77
  // operator to ignore the colour.
52
- var pillCls = !r ? 'text-bg-secondary'
53
- : (r.ok ? 'text-bg-success' : (r.level === 'bad' ? 'text-bg-danger' : 'text-bg-warning'));
78
+ var pillCls = !r ? _tone.mute
79
+ : (r.ok ? _tone.ok : (r.level === 'bad' ? _tone.bad : _tone.warn));
54
80
  var pillText = !r ? 'not checked' : (r.ok ? 'answering' : 'needs attention');
55
81
  %>
56
82
  <div class="card mb-3 <%= _open ? 'border-warning' : '' %>" data-panel="lmx-<%= inst.id %>"<% if (_open) { %> data-panel-attention<% } %>
57
- data-lmx-id="<%= inst.id %>" data-lmx-label="<%= inst.label || inst.id %>" data-lmx-url="<%= inst.status_url %>">
83
+ data-lmx-id="<%= inst.id %>" data-lmx-label="<%= inst.label || inst.id %>" data-lmx-url="<%= inst.status_url %>"
84
+ <%# WHAT EDIT PREFILLS. The certificate IS carried, because it is public — it is not even
85
+ encrypted at rest — and an operator opening Edit to a blank certificate box has no way to
86
+ tell a stored one from none at all. The two tokens are carried only as LENGTHS, which is
87
+ the most that can be said about a write-only secret and, as it happens, exactly enough to
88
+ notice the same value pasted into both fields. %>
89
+ data-lmx-cert="<%= inst.caCert || '' %>"
90
+ data-lmx-token-len="<%= (inst.creds && inst.creds.statusTokenLength) || '' %>"
91
+ data-lmx-key-len="<%= (inst.creds && inst.creds.enginesKeyLength) || '' %>"
92
+ data-stack="<%= inst.id %>"<% if (_isNew) { %> data-stack-new hidden<% } %>>
93
+ <% if (!_isNew) { %>
94
+ <div data-stack-view>
58
95
  <%# THE TOGGLE AND THE ACTIONS ARE SIBLINGS, not nested. A <button> may not contain a link
59
96
  or another button — the markup parses unpredictably and the inner control stops being
60
97
  reachable by keyboard, which would put Check and Edit behind a mouse. %>
61
98
  <div class="d-flex gap-2 align-items-start p-3">
62
99
  <button type="button" class="btn text-start border-0 p-0 flex-grow-1 d-flex gap-2 align-items-start"
100
+ style="min-width:0"
63
101
  data-panel-toggle="lmx-<%= inst.id %>" aria-expanded="<%= _open ? 'true' : 'false' %>"
64
102
  aria-controls="body-lmx-<%= inst.id %>">
65
103
  <span class="panel-chev text-body-secondary" aria-hidden="true">&rsaquo;</span>
@@ -67,12 +105,12 @@
67
105
  <span class="d-flex align-items-center gap-2 flex-wrap">
68
106
  <strong><%= inst.label || inst.id %></strong>
69
107
  <code class="small"><%= inst.id %></code>
70
- <span class="badge text-bg-light border">lmx</span>
71
- <span class="badge <%= pillCls %>"><%= pillText %></span>
108
+ <span class="<%= _role %>">lmx</span>
109
+ <span class="<%= _pill %> <%= pillCls %>"><%- _dot %> <%= pillText %></span>
72
110
  <%# THE COUNT IS "WHAT AM I RELYING ON", not what exists. Active means added, enabled
73
111
  and healthy in the stack's own document — the number that was missing when a
74
112
  configured-but-broken engine looked exactly like a working one. %>
75
- <span class="badge text-bg-light border font-monospace">
113
+ <span class="<%= _count %>">
76
114
  <% if (r && r.engines) { %>
77
115
  <%= inst.activeCount %> engine<%= inst.activeCount === 1 ? '' : 's' %> active · <%= inst.reportedCount %> reported<% if (inst.missingCount) { %> · <span class="text-danger"><%= inst.missingCount %> missing</span><% } %>
78
116
  <% } else if (r) { %>
@@ -91,18 +129,27 @@
91
129
  · key <%= inst.creds.enginesKey ? '✔' : '✖' %>
92
130
  · cert <% if (!inst.pin.present) { %>none<% } else if (!inst.pin.valid) { %><span class="text-danger">unreadable</span><% } else if (inst.pin.expired) { %><span class="text-danger">EXPIRED</span><% } else if (inst.pin.expiringSoon) { %><span class="text-warning"><%= inst.pin.expiresDays %>d left</span><% } else { %>✔<% } %>
93
131
  <% if (r) { %>· checked <%= _ago(r.at) %><% } %>
94
- <% if (!inst.enabled) { %>· <span class="badge text-bg-secondary">disabled</span><% } %>
132
+ <% if (!inst.enabled) { %>· <span class="<%= _pill %> <%= _tone.mute %>">disabled</span><% } %>
95
133
  </span>
96
134
  </span>
97
135
  </button>
98
136
  <% if (_edit) { %>
99
- <span class="d-flex gap-2 align-items-center">
137
+ <span class="d-flex gap-2 align-items-center flex-shrink-0">
100
138
  <form method="POST" action="<%= _base %>/lmx/<%= inst.id %>/check">
101
139
  <input type="hidden" name="_csrf" value="<%= _csrf() %>">
102
- <button type="submit" class="btn btn-sm btn-outline-secondary">Check</button>
140
+ <button type="submit" class="btn btn-sm btn-outline-secondary">Refresh</button>
141
+ </form>
142
+ <%# EVERY ACTION FOR THIS STACK IN ONE PLACE. There used to be a second Refresh at the foot
143
+ of the panel, called "Check now" — the same POST to the same route, so not a second
144
+ control at all, just a second place to look for the first one. And Remove sat down there
145
+ alone, which put the most destructive action furthest from the thing it destroys. %>
146
+ <button type="button" class="btn btn-sm btn-outline-secondary"
147
+ data-stack-edit-open="<%= inst.id %>">Edit</button>
148
+ <form method="POST" action="<%= _base %>/lmx/<%= inst.id %>/delete"
149
+ data-confirm="Remove the &ldquo;<%= inst.label || inst.id %>&rdquo; supervisor?">
150
+ <input type="hidden" name="_csrf" value="<%= _csrf() %>">
151
+ <button type="submit" class="btn btn-sm btn-outline-danger">Remove</button>
103
152
  </form>
104
- <a class="btn btn-sm btn-outline-secondary" href="<%= _anchor %>"
105
- data-lmx-edit="<%= inst.id %>">Edit</a>
106
153
  </span>
107
154
  <% } %>
108
155
  </div>
@@ -130,43 +177,55 @@
130
177
  <%# NOT PROBED ON RENDER, deliberately: opening a settings page is not evidence that
131
178
  anything changed, and a sleeping machine would hold the whole page while it timed
132
179
  out. Check asks, when somebody wants to know. %>
133
- Press <strong>Check</strong> to ask it now.
180
+ Press <strong>Refresh</strong> to ask it now.
134
181
  </div>
135
182
  <% } %>
136
183
 
137
184
  <%# ── ENGINES IN USE ── %>
138
185
  <% if (inst.added.length) { %>
139
186
  <div class="px-3 py-2 bg-body-tertiary d-flex justify-content-between small text-body-secondary">
140
- <span>In use — <%= inst.added.length %></span>
141
- <span>endpoint · jobs served</span>
187
+ <span class="text-uppercase fw-semibold" style="letter-spacing:.08em">In use — <%= inst.added.length %> engine<%= inst.added.length === 1 ? "" : "s" %></span>
188
+ <span class="text-uppercase fw-semibold" style="letter-spacing:.08em">endpoint · jobs served</span>
142
189
  </div>
143
190
  <% inst.added.forEach(function (a) { %>
144
- <div class="d-flex justify-content-between align-items-start gap-3 border-top p-3 <%= a.missing ? 'bg-danger-subtle' : '' %>">
191
+ <div class="d-flex justify-content-between align-items-start gap-3 border-top p-3<% if (a.missing) { %> bg-danger-subtle<% } %>"<% if (!a.missing) { %> style="<%= _inUse %>"<% } %>>
145
192
  <div>
146
193
  <div class="d-flex align-items-center gap-2 flex-wrap">
147
194
  <strong><%= a.row.lmx_engine %></strong>
148
195
  <% if (a.missing) { %>
149
- <span class="badge text-bg-danger">not reported</span>
196
+ <span class="<%= _pill %> <%= _tone.bad %>"><%- _dot %> not reported</span>
150
197
  <% } else if (a.engine) { %>
151
- <span class="badge <%= a.engine.state === 'healthy' ? 'text-bg-success' : 'text-bg-warning' %>"><%= a.engine.state %></span>
152
- <% if (a.engine.role) { %><span class="badge text-bg-light border"><%= a.engine.role %></span><% } %>
198
+ <span class="<%= _pill %> <%= a.engine.state === 'healthy' ? _tone.ok : _tone.warn %>"><%- _dot %> <%= a.engine.state %></span>
199
+ <% if (a.engine.role) { %><span class="<%= _role %>"><%= a.engine.role %></span><% } %>
153
200
  <% } %>
154
- <% if (!Number(a.row.enabled)) { %><span class="badge text-bg-secondary">disabled</span><% } %>
201
+ <% if (!Number(a.row.enabled)) { %><span class="<%= _pill %> <%= _tone.mute %>">disabled</span><% } %>
155
202
  </div>
156
203
  <% if (a.engine) { %>
157
204
  <div class="small text-body-tertiary font-monospace mt-1">
158
- <%= [a.engine.modelName || a.engine.model, a.engine.quantisation, a.engine.device,
205
+ <%= [a.engine.modelName || a.engine.model, a.engine.quantisation,
206
+ a.engine.parameters ? (a.engine.parameters / 1e9).toFixed(1).replace(/\.0$/, '') + 'B' : null,
207
+ a.engine.device,
208
+ a.engine.deviceTotalMiB ? Math.round(a.engine.deviceTotalMiB / 1024) + ' GiB' : null,
159
209
  a.engine.maxInputTokens ? Number(a.engine.maxInputTokens).toLocaleString() + ' tok/slot' : null,
160
210
  a.engine.slots ? a.engine.slots + (a.engine.slots === 1 ? ' slot' : ' slots') : null,
161
211
  a.engine.dimensions ? a.engine.dimensions + ' dims' : null]
162
212
  .filter(Boolean).join(' · ') %>
163
213
  </div>
214
+ <%# WHICH FLAG THIS ENGINE GETS, on an engine already being relied on — not only on
215
+ one somebody might add. Getting it wrong is silent: the model spends its whole
216
+ budget thinking and returns nothing, with no error at all, and this row is the
217
+ only place anybody would look to find out which flag is being sent. %>
218
+ <% if (a.engine.reasoning) { %>
219
+ <div class="small text-body-secondary mt-1">reasoning: <code><%= a.engine.reasoning %></code></div>
220
+ <% } else if (a.engine.role === 'chat') { %>
221
+ <div class="small text-warning mt-1">no known reasoning flag for this model family — neither will be sent</div>
222
+ <% } %>
164
223
  <% } %>
165
224
  <%# WHAT STILL POINTS AT IT. A missing engine whose jobs nobody has repointed is
166
225
  an endpoint that will fail every call — the same silent-but-configured shape
167
226
  as the credential problem, so it names the jobs rather than only itself. %>
168
227
  <div class="small mt-1 <%= a.missing ? 'text-danger fw-semibold' : 'text-body-secondary' %>">
169
- <a href="<%= _base %>?edit=<%= encodeURIComponent(a.row.id) %>" class="text-decoration-none"><%= a.row.id %></a>
228
+ <span class="font-monospace"><%= a.row.id %></span>
170
229
  <% if (a.routes.length) { %>
171
230
  · <%= a.routes.map(function (x) { return _label(x.id); }).join(', ') %>
172
231
  <% } else { %>
@@ -180,7 +239,36 @@
180
239
  <% } %>
181
240
  </div>
182
241
  <div class="d-flex gap-2">
183
- <a class="btn btn-sm btn-outline-secondary" href="<%= _base %>?edit=<%= encodeURIComponent(a.row.id) %>">Settings</a>
242
+ <%# TEST THIS ENGINE, not whatever is currently serving. An engine you have adopted is an
243
+ endpoint like any other, and the reason to test one is precisely that it is NOT the
244
+ one answering right now — verifying a fallback before it is needed is the point of
245
+ having one.
246
+
247
+ THERE IS NO "SETTINGS" HERE, and that is deliberate. It used to link to the generic
248
+ endpoint form, which knows nothing about lmx: of the eight fields it offers, an
249
+ address and a model are DISCOVERED for these engines and a context size is
250
+ overridden by what the supervisor reports — so five of them do nothing, and two of
251
+ those look exactly like the settings that matter. A control that leads somewhere
252
+ describing a stored address, for a thing whose whole design is that it stores no
253
+ address, is worse than no control.
254
+
255
+ The genuinely per-engine settings — a token ceiling, a timeout, its own key — are
256
+ real and will come back as an inline form on this row, like everything else on this
257
+ screen. Until they do, an engine is removed and re-added rather than edited. %>
258
+ <form method="POST" action="<%= _base %>/<%= encodeURIComponent(a.row.id) %>/test">
259
+ <input type="hidden" name="_csrf" value="<%= _csrf() %>">
260
+ <button type="submit" class="btn btn-sm btn-outline-secondary">Test</button>
261
+ </form>
262
+ <%# REMOVE IS THE INVERSE OF ADD, and the panel has had Add since the beginning. Without
263
+ it an adopted engine could be created here and unmade nowhere: lmx rows are excluded
264
+ from the direct-endpoint list on purpose, so this panel is the only place they
265
+ appear at all. A missing engine's row even says "remove the endpoint" — and until
266
+ now offered nothing to press. %>
267
+ <form method="POST" action="<%= _base %>/<%= encodeURIComponent(a.row.id) %>/delete"
268
+ data-confirm="Remove the &ldquo;<%= a.row.id %>&rdquo; endpoint?<% if (a.routes.length) { %> It still serves <%= a.routes.length %> job<%= a.routes.length === 1 ? '' : 's' %>, which will fall back to whatever else those jobs list.<% } %>">
269
+ <input type="hidden" name="_csrf" value="<%= _csrf() %>">
270
+ <button type="submit" class="btn btn-sm btn-outline-danger">Remove</button>
271
+ </form>
184
272
  </div>
185
273
  </div>
186
274
  <% }); %>
@@ -189,8 +277,8 @@
189
277
  <%# ── ENGINES AVAILABLE ── %>
190
278
  <% if (inst.available && inst.available.length) { %>
191
279
  <div class="px-3 py-2 bg-body-tertiary d-flex justify-content-between small text-body-secondary border-top">
192
- <span>Also on this stack — not in use</span>
193
- <span><%= inst.available.length %></span>
280
+ <span class="text-uppercase fw-semibold" style="letter-spacing:.08em">Also on this stack — not in use</span>
281
+ <span class="text-uppercase fw-semibold" style="letter-spacing:.08em"><%= inst.available.length %></span>
194
282
  </div>
195
283
  <% inst.available.forEach(function (e) { %>
196
284
  <div class="d-flex justify-content-between align-items-start gap-3 border-top p-3">
@@ -198,8 +286,8 @@
198
286
  <div class="d-flex align-items-center gap-2 flex-wrap">
199
287
  <strong><%= e.label || e.name %></strong>
200
288
  <code class="small"><%= e.name %></code>
201
- <span class="badge <%= e.state === 'healthy' ? 'text-bg-success' : 'text-bg-warning' %>"><%= e.state || 'unknown' %></span>
202
- <span class="badge text-bg-light border"><%= e.role || '?' %></span>
289
+ <span class="<%= _pill %> <%= e.state === 'healthy' ? _tone.ok : _tone.warn %>"><%- _dot %> <%= e.state || 'unknown' %></span>
290
+ <span class="<%= _role %>"><%= e.role || '?' %></span>
203
291
  </div>
204
292
  <div class="small text-body-tertiary font-monospace mt-1">
205
293
  <%= [e.modelName || e.model, e.quantisation, e.device,
@@ -229,36 +317,104 @@
229
317
  <% }); %>
230
318
  <% } %>
231
319
 
232
- <%# ── CREDENTIALS: what is set, never what it is ── %>
233
- <div class="px-3 py-2 bg-body-tertiary border-top small text-body-secondary">Credentials</div>
234
- <div class="p-3 border-top d-flex gap-2 flex-wrap">
235
- <span class="badge text-bg-light border font-monospace">
236
- <span class="<%= inst.creds.statusToken ? 'text-success' : 'text-danger' %>"><%= inst.creds.statusToken ? '✔' : '✖' %></span>
237
- status token
238
- </span>
239
- <span class="badge text-bg-light border font-monospace">
240
- <span class="<%= inst.creds.enginesKey ? 'text-success' : 'text-danger' %>"><%= inst.creds.enginesKey ? '✔' : '✖' %></span>
241
- engines key
242
- </span>
243
- <span class="badge <%= (inst.pin.expired || !inst.pin.valid) ? 'text-bg-danger' : (inst.pin.expiringSoon ? 'text-bg-warning' : 'text-bg-light border') %> font-monospace">
244
- <% if (!inst.pin.present) { %>no certificate pinned
245
- <% } else if (!inst.pin.valid) { %>certificate unreadable
246
- <% } else { %>cert <%= inst.pin.fingerprint %> · expires <%= inst.pin.expires.toISOString().slice(0, 10) %><% if (inst.pin.expiringSoon) { %> · <%= inst.pin.expiresDays %>d left<% } %><% } %>
247
- </span>
248
320
  </div>
321
+ </div>
322
+ <% } %>
249
323
 
250
- <% if (_edit) { %>
251
- <div class="p-3 border-top d-flex gap-2">
252
- <form method="POST" action="<%= _base %>/lmx/<%= inst.id %>/check">
253
- <input type="hidden" name="_csrf" value="<%= _csrf() %>">
254
- <button type="submit" class="btn btn-sm btn-outline-secondary">Check now</button>
255
- </form>
256
- <form method="POST" action="<%= _base %>/lmx/<%= inst.id %>/delete"
257
- data-confirm="Remove the &ldquo;<%= inst.label || inst.id %>&rdquo; supervisor?">
258
- <input type="hidden" name="_csrf" value="<%= _csrf() %>">
259
- <button type="submit" class="btn btn-sm btn-outline-danger">Remove</button>
260
- </form>
324
+ <%# ── EDITING, IN PLACE ──────────────────────────────────────────────────────────────────────
325
+ NOT A FORM AT THE FOOT OF THE PAGE. It was one, and pressing Edit scrolled you away from the
326
+ stack you were looking at, down to a form that also served as "Add" so the thing being
327
+ changed was off screen while you changed it, and nothing on it said which stack you were in.
328
+
329
+ Same two-mode shape as the job card: the panel becomes the form, Save posts, and the redirect
330
+ brings the panel back. %>
331
+ <% if (_edit) { %>
332
+ <form method="POST" action="<%= _base %>/lmx" class="p-3" data-stack-edit<% if (!_isNew) { %> hidden<% } %>>
333
+ <input type="hidden" name="_csrf" value="<%= _csrf() %>">
334
+
335
+ <div class="d-flex justify-content-between align-items-baseline mb-3">
336
+ <strong><%= _isNew ? 'Add a supervised stack' : 'Change ' + (inst.label || inst.id) %></strong>
337
+ <span class="small text-body-tertiary">blank secrets keep what is stored</span>
261
338
  </div>
262
- <% } %>
263
- </div>
339
+
340
+ <div class="row g-3">
341
+ <div class="col-md-4">
342
+ <label class="form-label" for="lmx-id-<%= inst.id %>">Instance</label>
343
+ <%# THE STACK'S OWN NAME, checked against what the status document reports — which is what
344
+ catches a URL pointed at the wrong deployment, since engine names collide across
345
+ stacks. Readonly once it exists: every engine row references it, and it is the value
346
+ the identity check compares against. %>
347
+ <input id="lmx-id-<%= inst.id %>" type="text" class="form-control font-monospace"
348
+ name="id" value="<%= _isNew ? '' : inst.id %>" placeholder="vpc1-dev" required
349
+ <% if (!_isNew) { %>readonly<% } %>>
350
+ <div class="form-text">
351
+ <% if (_isNew) { %>Exactly as the stack reports it. Checked on every poll.
352
+ <% } else { %>Fixed — engines reference it, and the identity check compares against it.<% } %>
353
+ </div>
354
+ </div>
355
+ <div class="col-md-8">
356
+ <label class="form-label" for="lmx-label-<%= inst.id %>">Name</label>
357
+ <input id="lmx-label-<%= inst.id %>" type="text" class="form-control" name="label"
358
+ value="<%= _isNew ? '' : (inst.label || '') %>" placeholder="Dev stack">
359
+ </div>
360
+ <div class="col-12">
361
+ <label class="form-label" for="lmx-url-<%= inst.id %>">Status listener</label>
362
+ <input id="lmx-url-<%= inst.id %>" type="text" class="form-control font-monospace" name="status_url"
363
+ value="<%= _isNew ? '' : (inst.status_url || '') %>"
364
+ placeholder="https://vpc1.example.net:9443/status" required>
365
+ <div class="form-text">Not an engine address — this is only asked where the engines are.</div>
366
+ </div>
367
+
368
+ <%# THE LENGTHS ARE THE POINT. Both are write-only, so the only thing that can be said about
369
+ a stored secret is how long it is — and two fields offering to keep the same number of
370
+ characters is exactly how an engines key pasted into both of them became visible. %>
371
+ <div class="col-md-6">
372
+ <label class="form-label" for="lmx-token-<%= inst.id %>">Status token</label>
373
+ <input id="lmx-token-<%= inst.id %>" type="password" class="form-control" name="status_token"
374
+ autocomplete="new-password"
375
+ placeholder="<%= (inst.creds && inst.creds.statusTokenLength) ? 'unchanged — ' + inst.creds.statusTokenLength + ' characters stored' : 'nothing stored — enter a value' %>">
376
+ <div class="form-text">Reads the status document. Never sent to an engine.</div>
377
+ </div>
378
+ <div class="col-md-6">
379
+ <label class="form-label" for="lmx-key-<%= inst.id %>">Engines key</label>
380
+ <input id="lmx-key-<%= inst.id %>" type="password" class="form-control" name="engines_key"
381
+ autocomplete="new-password"
382
+ placeholder="<%= (inst.creds && inst.creds.enginesKeyLength) ? 'unchanged — ' + inst.creds.enginesKeyLength + ' characters stored' : 'nothing stored — enter a value' %>">
383
+ <div class="form-text">Used by every engine here unless one overrides it.</div>
384
+ </div>
385
+
386
+ <div class="col-12">
387
+ <label class="form-label" for="lmx-ca-<%= inst.id %>">Certificate to pin</label>
388
+ <%# PINNED, never "verification off". Turning verification off accepts any certificate from
389
+ anyone able to answer on that address, which is the whole attack pinning prevents. The
390
+ certificate is public, so unlike the tokens it is shown back. %>
391
+ <textarea id="lmx-ca-<%= inst.id %>" class="form-control font-monospace" name="ca_cert" rows="3"
392
+ placeholder="-----BEGIN CERTIFICATE-----"><%= inst.caCert || '' %></textarea>
393
+ <div class="form-text">
394
+ The stack's own certificate. Leave blank if it uses one your system already trusts.
395
+ <% if (!_isNew && inst.pin && inst.pin.valid) { %>
396
+ Pinned now: <span class="font-monospace"><%= inst.pin.fingerprint %></span>,
397
+ expires <%= inst.pin.expires.toISOString().slice(0, 10) %>.
398
+ <% } %>
399
+ </div>
400
+ </div>
401
+
402
+ <div class="col-12">
403
+ <div class="form-check form-switch">
404
+ <input class="form-check-input" type="checkbox" role="switch" name="enabled" value="1"
405
+ id="lmx-enabled-<%= inst.id %>"<% if (_isNew || inst.enabled) { %> checked<% } %>>
406
+ <label class="form-check-label" for="lmx-enabled-<%= inst.id %>">Enabled</label>
407
+ </div>
408
+ </div>
409
+ </div>
410
+
411
+ <div class="d-flex gap-2 mt-3">
412
+ <button type="submit" class="btn btn-sm btn-primary"><%= _isNew ? 'Add stack' : 'Save' %></button>
413
+ <button type="button" class="btn btn-sm btn-outline-secondary" data-stack-cancel>Cancel</button>
414
+ </div>
415
+ <div class="form-text mt-2">
416
+ Every save is checked against the stack, and the per-check result appears on this panel.
417
+ </div>
418
+ </form>
419
+ <% } %>
264
420
  </div>