@aria-framework/ai 0.15.0 → 0.15.2

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.
@@ -1,113 +1,131 @@
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
+ // 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. The
90
+ // certificate is NOT one of them — it is public, and is prefilled below.
91
+ var edit = ev.target.closest('[data-lmx-edit]');
92
+ if (!edit) return;
93
+ var src = document.querySelector('[data-lmx-id="' + edit.getAttribute('data-lmx-edit') + '"]');
94
+ var form = document.querySelector('#lmx-new form');
95
+ if (!src || !form) return;
96
+ var set = function (name, value) {
97
+ var field = form.querySelector('[name="' + name + '"]');
98
+ if (field) field.value = value == null ? '' : value;
99
+ };
100
+ set('id', src.getAttribute('data-lmx-id'));
101
+ set('label', src.getAttribute('data-lmx-label'));
102
+ set('status_url', src.getAttribute('data-lmx-url'));
103
+ // THE CERTIFICATE COMES BACK. It is public — not encrypted at rest — and Edit opening onto an
104
+ // empty certificate box is indistinguishable from having none stored, which reads as "my save
105
+ // did not work". Reported as exactly that, and it was: the save handler had always intended to
106
+ // render it back, and never did.
107
+ set('ca_cert', src.getAttribute('data-lmx-cert'));
108
+ set('status_token', '');
109
+ set('engines_key', '');
110
+ // Blank means keep, so blank must not read as absent. The placeholder says what is actually
111
+ // held — and two fields both offering to keep "50 characters" is the cheapest possible way to
112
+ // notice that one value was pasted into both.
113
+ var held = function (name, len) {
114
+ var field = form.querySelector('[name="' + name + '"]');
115
+ if (!field) return;
116
+ field.placeholder = len
117
+ ? 'unchanged — ' + len + ' characters stored. Enter a value to replace it.'
118
+ : 'nothing stored — enter a value';
119
+ };
120
+ held('status_token', src.getAttribute('data-lmx-token-len'));
121
+ held('engines_key', src.getAttribute('data-lmx-key-len'));
122
+ var heading = document.querySelector('#lmx-new .card-title');
123
+ if (heading) heading.textContent = 'Change ' + (src.getAttribute('data-lmx-label') || src.getAttribute('data-lmx-id'));
124
+ });
125
+
126
+ if (document.readyState === 'loading') {
127
+ document.addEventListener('DOMContentLoaded', restore);
128
+ } else {
129
+ restore();
130
+ }
131
+ })();
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.15.2",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
7
7
  "publishConfig": {
@@ -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,17 +44,47 @@
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
+
47
69
  var r = inst.report;
48
70
  var _open = !!inst.attention;
49
71
  // NOT CHECKED is its own state, distinct from both answering and broken. A stack nobody has asked
50
72
  // since the last restart is not a stack in trouble, and colouring it as one would train an
51
73
  // 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'));
74
+ var pillCls = !r ? _tone.mute
75
+ : (r.ok ? _tone.ok : (r.level === 'bad' ? _tone.bad : _tone.warn));
54
76
  var pillText = !r ? 'not checked' : (r.ok ? 'answering' : 'needs attention');
55
77
  %>
56
78
  <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 %>">
79
+ data-lmx-id="<%= inst.id %>" data-lmx-label="<%= inst.label || inst.id %>" data-lmx-url="<%= inst.status_url %>"
80
+ <%# WHAT EDIT PREFILLS. The certificate IS carried, because it is public — it is not even
81
+ encrypted at rest — and an operator opening Edit to a blank certificate box has no way to
82
+ tell a stored one from none at all. The two tokens are carried only as LENGTHS, which is
83
+ the most that can be said about a write-only secret and, as it happens, exactly enough to
84
+ notice the same value pasted into both fields. %>
85
+ data-lmx-cert="<%= inst.caCert || '' %>"
86
+ data-lmx-token-len="<%= (inst.creds && inst.creds.statusTokenLength) || '' %>"
87
+ data-lmx-key-len="<%= (inst.creds && inst.creds.enginesKeyLength) || '' %>">
58
88
  <%# THE TOGGLE AND THE ACTIONS ARE SIBLINGS, not nested. A <button> may not contain a link
59
89
  or another button — the markup parses unpredictably and the inner control stops being
60
90
  reachable by keyboard, which would put Check and Edit behind a mouse. %>
@@ -67,12 +97,12 @@
67
97
  <span class="d-flex align-items-center gap-2 flex-wrap">
68
98
  <strong><%= inst.label || inst.id %></strong>
69
99
  <code class="small"><%= inst.id %></code>
70
- <span class="badge text-bg-light border">lmx</span>
71
- <span class="badge <%= pillCls %>"><%= pillText %></span>
100
+ <span class="<%= _role %>">lmx</span>
101
+ <span class="<%= _pill %> <%= pillCls %>"><%- _dot %> <%= pillText %></span>
72
102
  <%# THE COUNT IS "WHAT AM I RELYING ON", not what exists. Active means added, enabled
73
103
  and healthy in the stack's own document — the number that was missing when a
74
104
  configured-but-broken engine looked exactly like a working one. %>
75
- <span class="badge text-bg-light border font-monospace">
105
+ <span class="<%= _count %>">
76
106
  <% if (r && r.engines) { %>
77
107
  <%= inst.activeCount %> engine<%= inst.activeCount === 1 ? '' : 's' %> active · <%= inst.reportedCount %> reported<% if (inst.missingCount) { %> · <span class="text-danger"><%= inst.missingCount %> missing</span><% } %>
78
108
  <% } else if (r) { %>
@@ -91,7 +121,7 @@
91
121
  · key <%= inst.creds.enginesKey ? '✔' : '✖' %>
92
122
  · 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
123
  <% if (r) { %>· checked <%= _ago(r.at) %><% } %>
94
- <% if (!inst.enabled) { %>· <span class="badge text-bg-secondary">disabled</span><% } %>
124
+ <% if (!inst.enabled) { %>· <span class="<%= _pill %> <%= _tone.mute %>">disabled</span><% } %>
95
125
  </span>
96
126
  </span>
97
127
  </button>
@@ -99,7 +129,7 @@
99
129
  <span class="d-flex gap-2 align-items-center">
100
130
  <form method="POST" action="<%= _base %>/lmx/<%= inst.id %>/check">
101
131
  <input type="hidden" name="_csrf" value="<%= _csrf() %>">
102
- <button type="submit" class="btn btn-sm btn-outline-secondary">Check</button>
132
+ <button type="submit" class="btn btn-sm btn-outline-secondary">Refresh</button>
103
133
  </form>
104
134
  <a class="btn btn-sm btn-outline-secondary" href="<%= _anchor %>"
105
135
  data-lmx-edit="<%= inst.id %>">Edit</a>
@@ -130,37 +160,49 @@
130
160
  <%# NOT PROBED ON RENDER, deliberately: opening a settings page is not evidence that
131
161
  anything changed, and a sleeping machine would hold the whole page while it timed
132
162
  out. Check asks, when somebody wants to know. %>
133
- Press <strong>Check</strong> to ask it now.
163
+ Press <strong>Refresh</strong> to ask it now.
134
164
  </div>
135
165
  <% } %>
136
166
 
137
167
  <%# ── ENGINES IN USE ── %>
138
168
  <% if (inst.added.length) { %>
139
169
  <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>
170
+ <span class="text-uppercase fw-semibold" style="letter-spacing:.08em">In use — <%= inst.added.length %> engine<%= inst.added.length === 1 ? "" : "s" %></span>
171
+ <span class="text-uppercase fw-semibold" style="letter-spacing:.08em">endpoint · jobs served</span>
142
172
  </div>
143
173
  <% 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' : '' %>">
174
+ <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
175
  <div>
146
176
  <div class="d-flex align-items-center gap-2 flex-wrap">
147
177
  <strong><%= a.row.lmx_engine %></strong>
148
178
  <% if (a.missing) { %>
149
- <span class="badge text-bg-danger">not reported</span>
179
+ <span class="<%= _pill %> <%= _tone.bad %>"><%- _dot %> not reported</span>
150
180
  <% } 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><% } %>
181
+ <span class="<%= _pill %> <%= a.engine.state === 'healthy' ? _tone.ok : _tone.warn %>"><%- _dot %> <%= a.engine.state %></span>
182
+ <% if (a.engine.role) { %><span class="<%= _role %>"><%= a.engine.role %></span><% } %>
153
183
  <% } %>
154
- <% if (!Number(a.row.enabled)) { %><span class="badge text-bg-secondary">disabled</span><% } %>
184
+ <% if (!Number(a.row.enabled)) { %><span class="<%= _pill %> <%= _tone.mute %>">disabled</span><% } %>
155
185
  </div>
156
186
  <% if (a.engine) { %>
157
187
  <div class="small text-body-tertiary font-monospace mt-1">
158
- <%= [a.engine.modelName || a.engine.model, a.engine.quantisation, a.engine.device,
188
+ <%= [a.engine.modelName || a.engine.model, a.engine.quantisation,
189
+ a.engine.parameters ? (a.engine.parameters / 1e9).toFixed(1).replace(/\.0$/, '') + 'B' : null,
190
+ a.engine.device,
191
+ a.engine.deviceTotalMiB ? Math.round(a.engine.deviceTotalMiB / 1024) + ' GiB' : null,
159
192
  a.engine.maxInputTokens ? Number(a.engine.maxInputTokens).toLocaleString() + ' tok/slot' : null,
160
193
  a.engine.slots ? a.engine.slots + (a.engine.slots === 1 ? ' slot' : ' slots') : null,
161
194
  a.engine.dimensions ? a.engine.dimensions + ' dims' : null]
162
195
  .filter(Boolean).join(' · ') %>
163
196
  </div>
197
+ <%# WHICH FLAG THIS ENGINE GETS, on an engine already being relied on — not only on
198
+ one somebody might add. Getting it wrong is silent: the model spends its whole
199
+ budget thinking and returns nothing, with no error at all, and this row is the
200
+ only place anybody would look to find out which flag is being sent. %>
201
+ <% if (a.engine.reasoning) { %>
202
+ <div class="small text-body-secondary mt-1">reasoning: <code><%= a.engine.reasoning %></code></div>
203
+ <% } else if (a.engine.role === 'chat') { %>
204
+ <div class="small text-warning mt-1">no known reasoning flag for this model family — neither will be sent</div>
205
+ <% } %>
164
206
  <% } %>
165
207
  <%# WHAT STILL POINTS AT IT. A missing engine whose jobs nobody has repointed is
166
208
  an endpoint that will fail every call — the same silent-but-configured shape
@@ -180,6 +222,14 @@
180
222
  <% } %>
181
223
  </div>
182
224
  <div class="d-flex gap-2">
225
+ <%# TEST THIS ENGINE, not whatever is currently serving. An engine you have adopted is an
226
+ endpoint like any other, and the reason to test one is precisely that it is NOT the
227
+ one answering right now — verifying a fallback before it is needed is the point of
228
+ having one. %>
229
+ <form method="POST" action="<%= _base %>/<%= encodeURIComponent(a.row.id) %>/test">
230
+ <input type="hidden" name="_csrf" value="<%= _csrf() %>">
231
+ <button type="submit" class="btn btn-sm btn-outline-secondary">Test</button>
232
+ </form>
183
233
  <a class="btn btn-sm btn-outline-secondary" href="<%= _base %>?edit=<%= encodeURIComponent(a.row.id) %>">Settings</a>
184
234
  </div>
185
235
  </div>
@@ -189,8 +239,8 @@
189
239
  <%# ── ENGINES AVAILABLE ── %>
190
240
  <% if (inst.available && inst.available.length) { %>
191
241
  <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>
242
+ <span class="text-uppercase fw-semibold" style="letter-spacing:.08em">Also on this stack — not in use</span>
243
+ <span class="text-uppercase fw-semibold" style="letter-spacing:.08em"><%= inst.available.length %></span>
194
244
  </div>
195
245
  <% inst.available.forEach(function (e) { %>
196
246
  <div class="d-flex justify-content-between align-items-start gap-3 border-top p-3">
@@ -198,8 +248,8 @@
198
248
  <div class="d-flex align-items-center gap-2 flex-wrap">
199
249
  <strong><%= e.label || e.name %></strong>
200
250
  <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>
251
+ <span class="<%= _pill %> <%= e.state === 'healthy' ? _tone.ok : _tone.warn %>"><%- _dot %> <%= e.state || 'unknown' %></span>
252
+ <span class="<%= _role %>"><%= e.role || '?' %></span>
203
253
  </div>
204
254
  <div class="small text-body-tertiary font-monospace mt-1">
205
255
  <%= [e.modelName || e.model, e.quantisation, e.device,
@@ -229,18 +279,28 @@
229
279
  <% }); %>
230
280
  <% } %>
231
281
 
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>
282
+ <%# ── CREDENTIALS: what is set, never what it is ──
283
+ THE LENGTH IS SHOWN because it is the one property of a write-only secret that can be
284
+ checked by eye, and because two credentials of similar length in adjacent fields is how the
285
+ wrong one got pasted into both. Seeing "43 chars" beside a token you believe is 50 long is
286
+ the cheapest possible way to notice. It reveals nothing: a length is not a secret, and the
287
+ alternative was an operator with no way at all to tell two stored values apart. %>
288
+ <div class="px-3 py-2 bg-body-tertiary border-top small text-body-secondary d-flex justify-content-between align-items-center">
289
+ <span class="text-uppercase fw-semibold" style="letter-spacing:.08em">Credentials</span>
290
+ <% if (_edit) { %>
291
+ <a class="btn btn-sm btn-outline-secondary py-0" href="<%= _anchor %>" data-lmx-edit="<%= inst.id %>">Edit</a>
292
+ <% } %>
293
+ </div>
234
294
  <div class="p-3 border-top d-flex gap-2 flex-wrap">
235
- <span class="badge text-bg-light border font-monospace">
295
+ <span class="badge border bg-body-secondary text-body-secondary font-monospace fw-normal">
236
296
  <span class="<%= inst.creds.statusToken ? 'text-success' : 'text-danger' %>"><%= inst.creds.statusToken ? '✔' : '✖' %></span>
237
- status token
297
+ status token<% if (inst.creds.statusTokenLength) { %> · <%= inst.creds.statusTokenLength %> chars<% } %>
238
298
  </span>
239
- <span class="badge text-bg-light border font-monospace">
299
+ <span class="badge border bg-body-secondary text-body-secondary font-monospace fw-normal">
240
300
  <span class="<%= inst.creds.enginesKey ? 'text-success' : 'text-danger' %>"><%= inst.creds.enginesKey ? '✔' : '✖' %></span>
241
- engines key
301
+ engines key<% if (inst.creds.enginesKeyLength) { %> · <%= inst.creds.enginesKeyLength %> chars<% } %>
242
302
  </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">
303
+ <span class="badge border font-monospace fw-normal <%= (inst.pin.expired || !inst.pin.valid) ? _tone.bad : (inst.pin.expiringSoon ? _tone.warn : 'bg-body-secondary text-body-secondary') %>">
244
304
  <% if (!inst.pin.present) { %>no certificate pinned
245
305
  <% } else if (!inst.pin.valid) { %>certificate unreadable
246
306
  <% } else { %>cert <%= inst.pin.fingerprint %> · expires <%= inst.pin.expires.toISOString().slice(0, 10) %><% if (inst.pin.expiringSoon) { %> · <%= inst.pin.expiresDays %>d left<% } %><% } %>