@aria-framework/ai 0.25.0 → 0.26.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.
- package/README.md +60 -2
- package/browser/ai-panels.js +138 -138
- package/index.js +352 -309
- package/lmxStatus.js +1 -1
- package/lmxVerify.js +402 -402
- package/package.json +2 -2
- package/providers/anthropic.js +11 -4
- package/providers/lmx.js +52 -12
- package/providers/openai-compatible.js +37 -10
- package/views/ai/job-card.ejs +182 -182
- package/views/ai/lmx-stack.ejs +426 -426
package/views/ai/lmx-stack.ejs
CHANGED
|
@@ -1,426 +1,426 @@
|
|
|
1
|
-
<%# One supervised stack, as a folding panel.
|
|
2
|
-
────────────────────────────────────────────────────────────────────────────────────────────
|
|
3
|
-
A STACK IS A CONTAINER, not an endpoint. Its address is discovered, its model is a fact the
|
|
4
|
-
supervisor reports, and unfolding it lists engines rather than settings. The header reads
|
|
5
|
-
identity, kind, status, and then A COUNT OF WHAT DEPENDS ON IT — because the question anybody
|
|
6
|
-
has before touching a machine is "if this goes away, what stops working?".
|
|
7
|
-
|
|
8
|
-
FOLDED IS THE DEFAULT ONCE STABLE, so the folded line has to carry the whole verdict: status,
|
|
9
|
-
counts, all three credentials, and when it was last checked. Folding may hide detail; it must
|
|
10
|
-
never hide the reason you would have opened it. The app this was written for previously showed
|
|
11
|
-
a card with name, id and URL — none of which change when you paste a token — so a save that
|
|
12
|
-
worked and one that was discarded were pixel-identical.
|
|
13
|
-
|
|
14
|
-
A PANEL WITH A PROBLEM IS OPEN IN THE SERVER'S HTML, never opened later by script. If attention
|
|
15
|
-
depended on JavaScript, the one state that must never be missed would be the state that
|
|
16
|
-
disappears when a script fails to load.
|
|
17
|
-
|
|
18
|
-
Locals:
|
|
19
|
-
inst a supervisor row, merged with lmxStatus.describeStack() and carrying
|
|
20
|
-
`report` (lmxVerify), `pin` (lmxVerify.readPin), `creds` {statusToken, enginesKey}
|
|
21
|
-
canEdit render the controls at all
|
|
22
|
-
csrfToken () => token, for the forms
|
|
23
|
-
basePath where this app mounts its AI admin, e.g. '/admin/ai'. NOT hardcoded: the routes
|
|
24
|
-
are the consumer's, and only the consumer knows where they live.
|
|
25
|
-
newAnchor where Edit sends somebody to change the stack, e.g. '#lmx-new'
|
|
26
|
-
routeLabel optional (id) => human name for a job; the id is used when absent
|
|
27
|
-
%>
|
|
28
|
-
<%
|
|
29
|
-
// DISTINCT NAMES, deliberately. `var canEdit = (typeof canEdit === "undefined") ? ... : canEdit`
|
|
30
|
-
// reads as a defaulting idiom and is not one: `var` hoists, so inside this template the name is
|
|
31
|
-
// already declared and undefined when the typeof runs. The test is therefore always true, the
|
|
32
|
-
// passed-in local is shadowed, and every consumer silently gets the default.
|
|
33
|
-
var _edit = (typeof canEdit !== 'undefined') && canEdit;
|
|
34
|
-
var _base = (typeof basePath !== 'undefined' && basePath) ? basePath : '/admin/ai';
|
|
35
|
-
var _anchor = (typeof newAnchor !== 'undefined' && newAnchor) ? newAnchor : '#lmx-new';
|
|
36
|
-
var _label = (typeof routeLabel === 'function') ? routeLabel : function (x) { return x; };
|
|
37
|
-
var _csrf = (typeof csrfToken === 'function') ? csrfToken : function () { return ''; };
|
|
38
|
-
var _ago = function (iso) {
|
|
39
|
-
if (!iso) return null;
|
|
40
|
-
var secs = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 1000));
|
|
41
|
-
if (secs < 60) return secs + 's ago';
|
|
42
|
-
if (secs < 3600) return Math.round(secs / 60) + 'm ago';
|
|
43
|
-
return Math.round(secs / 3600) + 'h ago';
|
|
44
|
-
};
|
|
45
|
-
var _glyph = { pass: '✔', fail: '✖', warn: '⚠', skip: '—' };
|
|
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;
|
|
73
|
-
var r = inst.report;
|
|
74
|
-
var _open = !!inst.attention;
|
|
75
|
-
// NOT CHECKED is its own state, distinct from both answering and broken. A stack nobody has asked
|
|
76
|
-
// since the last restart is not a stack in trouble, and colouring it as one would train an
|
|
77
|
-
// operator to ignore the colour.
|
|
78
|
-
var pillCls = !r ? _tone.mute
|
|
79
|
-
: (r.ok ? _tone.ok : (r.level === 'bad' ? _tone.bad : _tone.warn));
|
|
80
|
-
var pillText = !r ? 'not checked' : (r.ok ? 'answering' : 'needs attention');
|
|
81
|
-
%>
|
|
82
|
-
<div class="card mb-3 <%= _open ? 'border-warning' : '' %>" data-panel="lmx-<%= inst.id %>"<% if (_open) { %> data-panel-attention<% } %>
|
|
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 enough for an operator to see
|
|
88
|
-
whether the two match: they SHOULD through the gateway (one lmxk_ key in both fields) and
|
|
89
|
-
should NOT direct to lmx (status.token and an engine key are different credentials). %>
|
|
90
|
-
data-lmx-cert="<%= inst.caCert || '' %>"
|
|
91
|
-
data-lmx-token-len="<%= (inst.creds && inst.creds.statusTokenLength) || '' %>"
|
|
92
|
-
data-lmx-key-len="<%= (inst.creds && inst.creds.enginesKeyLength) || '' %>"
|
|
93
|
-
data-stack="<%= inst.id %>"<% if (_isNew) { %> data-stack-new hidden<% } %>>
|
|
94
|
-
<% if (!_isNew) { %>
|
|
95
|
-
<div data-stack-view>
|
|
96
|
-
<%# THE TOGGLE AND THE ACTIONS ARE SIBLINGS, not nested. A <button> may not contain a link
|
|
97
|
-
or another button — the markup parses unpredictably and the inner control stops being
|
|
98
|
-
reachable by keyboard, which would put Check and Edit behind a mouse. %>
|
|
99
|
-
<div class="d-flex gap-2 align-items-start p-3">
|
|
100
|
-
<button type="button" class="btn text-start border-0 p-0 flex-grow-1 d-flex gap-2 align-items-start"
|
|
101
|
-
style="min-width:0"
|
|
102
|
-
data-panel-toggle="lmx-<%= inst.id %>" aria-expanded="<%= _open ? 'true' : 'false' %>"
|
|
103
|
-
aria-controls="body-lmx-<%= inst.id %>">
|
|
104
|
-
<span class="panel-chev text-body-secondary" aria-hidden="true">›</span>
|
|
105
|
-
<span class="flex-grow-1">
|
|
106
|
-
<span class="d-flex align-items-center gap-2 flex-wrap">
|
|
107
|
-
<strong><%= inst.label || inst.id %></strong>
|
|
108
|
-
<code class="small"><%= inst.id %></code>
|
|
109
|
-
<span class="<%= _role %>">lmx</span>
|
|
110
|
-
<span class="<%= _pill %> <%= pillCls %>"><%- _dot %> <%= pillText %></span>
|
|
111
|
-
<%# THE COUNT IS "WHAT AM I RELYING ON", not what exists. Active means added, enabled
|
|
112
|
-
and healthy in the stack's own document — the number that was missing when a
|
|
113
|
-
configured-but-broken engine looked exactly like a working one. %>
|
|
114
|
-
<span class="<%= _count %>">
|
|
115
|
-
<% if (r && r.engines) { %>
|
|
116
|
-
<%= inst.activeCount %> engine<%= inst.activeCount === 1 ? '' : 's' %> active · <%= inst.reportedCount %> reported<% if (inst.missingCount) { %> · <span class="text-danger"><%= inst.missingCount %> missing</span><% } %>
|
|
117
|
-
<% } else if (r) { %>
|
|
118
|
-
<%# CHECKED, BUT THE LIST DID NOT ARRIVE. "not checked yet" here would be a lie
|
|
119
|
-
about something that just happened 0 seconds ago, and would send somebody to
|
|
120
|
-
press Check again rather than read the reason sitting directly below. %>
|
|
121
|
-
<%= inst.added.length %> added · engines could not be listed
|
|
122
|
-
<% } else { %>
|
|
123
|
-
<%= inst.added.length %> added · not checked yet
|
|
124
|
-
<% } %>
|
|
125
|
-
</span>
|
|
126
|
-
</span>
|
|
127
|
-
<span class="d-block small text-body-secondary font-monospace mt-1" style="word-break:break-all">
|
|
128
|
-
<%= inst.status_url %>
|
|
129
|
-
· token <%= inst.creds.statusToken ? '✔' : '✖' %>
|
|
130
|
-
· key <%= inst.creds.enginesKey ? '✔' : '✖' %>
|
|
131
|
-
· 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 { %>✔<% } %>
|
|
132
|
-
<% if (r) { %>· checked <%= _ago(r.at) %><% } %>
|
|
133
|
-
<% if (!inst.enabled) { %>· <span class="<%= _pill %> <%= _tone.mute %>">disabled</span><% } %>
|
|
134
|
-
</span>
|
|
135
|
-
</span>
|
|
136
|
-
</button>
|
|
137
|
-
<% if (_edit) { %>
|
|
138
|
-
<span class="d-flex gap-2 align-items-center flex-shrink-0">
|
|
139
|
-
<form method="POST" action="<%= _base %>/lmx/<%= inst.id %>/check">
|
|
140
|
-
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
141
|
-
<button type="submit" class="btn btn-sm btn-outline-secondary">Refresh</button>
|
|
142
|
-
</form>
|
|
143
|
-
<%# EVERY ACTION FOR THIS STACK IN ONE PLACE. There used to be a second Refresh at the foot
|
|
144
|
-
of the panel, called "Check now" — the same POST to the same route, so not a second
|
|
145
|
-
control at all, just a second place to look for the first one. And Remove sat down there
|
|
146
|
-
alone, which put the most destructive action furthest from the thing it destroys. %>
|
|
147
|
-
<button type="button" class="btn btn-sm btn-outline-secondary"
|
|
148
|
-
data-stack-edit-open="<%= inst.id %>">Edit</button>
|
|
149
|
-
<form method="POST" action="<%= _base %>/lmx/<%= inst.id %>/delete"
|
|
150
|
-
data-confirm="Remove the “<%= inst.label || inst.id %>” supervisor?">
|
|
151
|
-
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
152
|
-
<button type="submit" class="btn btn-sm btn-outline-danger">Remove</button>
|
|
153
|
-
</form>
|
|
154
|
-
</span>
|
|
155
|
-
<% } %>
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
|
-
<div id="body-lmx-<%= inst.id %>" class="border-top"<% if (!_open) { %> hidden<% } %>>
|
|
159
|
-
|
|
160
|
-
<%# THE LAST VERIFICATION, per check. Never a single line: "401" named neither of the two
|
|
161
|
-
credentials it could have been, which is exactly how an engines key pasted into the
|
|
162
|
-
status token's field stayed hidden. %>
|
|
163
|
-
<% if (r) { %>
|
|
164
|
-
<div class="p-3 border-bottom">
|
|
165
|
-
<div class="small text-uppercase fw-semibold text-body-secondary mb-2" style="letter-spacing:.08em">
|
|
166
|
-
Last checked <%= _ago(r.at) %>
|
|
167
|
-
</div>
|
|
168
|
-
<% r.checks.forEach(function (c) { %>
|
|
169
|
-
<div class="d-flex gap-2 small font-monospace">
|
|
170
|
-
<span class="<%= _gcls[c.status] || '' %>" style="width:1rem"><%= _glyph[c.status] || '' %></span>
|
|
171
|
-
<span class="<%= c.status === 'skip' ? 'text-body-tertiary' : '' %>"><%= c.text %></span>
|
|
172
|
-
</div>
|
|
173
|
-
<% }); %>
|
|
174
|
-
</div>
|
|
175
|
-
<% } else { %>
|
|
176
|
-
<div class="p-3 border-bottom small text-body-secondary">
|
|
177
|
-
This stack has not been checked since the last restart.
|
|
178
|
-
<%# NOT PROBED ON RENDER, deliberately: opening a settings page is not evidence that
|
|
179
|
-
anything changed, and a sleeping machine would hold the whole page while it timed
|
|
180
|
-
out. Check asks, when somebody wants to know. %>
|
|
181
|
-
Press <strong>Refresh</strong> to ask it now.
|
|
182
|
-
</div>
|
|
183
|
-
<% } %>
|
|
184
|
-
|
|
185
|
-
<%# ── ENGINES IN USE ── %>
|
|
186
|
-
<% if (inst.added.length) { %>
|
|
187
|
-
<div class="px-3 py-2 bg-body-tertiary d-flex justify-content-between small text-body-secondary">
|
|
188
|
-
<span class="text-uppercase fw-semibold" style="letter-spacing:.08em">In use — <%= inst.added.length %> engine<%= inst.added.length === 1 ? "" : "s" %></span>
|
|
189
|
-
<span class="text-uppercase fw-semibold" style="letter-spacing:.08em">endpoint · jobs served</span>
|
|
190
|
-
</div>
|
|
191
|
-
<% inst.added.forEach(function (a) { %>
|
|
192
|
-
<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 %>"<% } %>>
|
|
193
|
-
<div>
|
|
194
|
-
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
195
|
-
<strong><%= a.row.lmx_engine %></strong>
|
|
196
|
-
<% if (a.missing) { %>
|
|
197
|
-
<span class="<%= _pill %> <%= _tone.bad %>"><%- _dot %> not reported</span>
|
|
198
|
-
<% } else if (a.engine) { %>
|
|
199
|
-
<span class="<%= _pill %> <%= a.engine.state === 'healthy' ? _tone.ok : _tone.warn %>"><%- _dot %> <%= a.engine.state %></span>
|
|
200
|
-
<% if (a.engine.role) { %><span class="<%= _role %>"><%= a.engine.role %></span><% } %>
|
|
201
|
-
<% } %>
|
|
202
|
-
<% if (!Number(a.row.enabled)) { %><span class="<%= _pill %> <%= _tone.mute %>">disabled</span><% } %>
|
|
203
|
-
</div>
|
|
204
|
-
<% if (a.engine) { %>
|
|
205
|
-
<div class="small text-body-tertiary font-monospace mt-1">
|
|
206
|
-
<%= [a.engine.modelName || a.engine.model, a.engine.quantisation,
|
|
207
|
-
a.engine.parameters ? (a.engine.parameters / 1e9).toFixed(1).replace(/\.0$/, '') + 'B' : null,
|
|
208
|
-
a.engine.device,
|
|
209
|
-
a.engine.deviceTotalMiB ? Math.round(a.engine.deviceTotalMiB / 1024) + ' GiB' : null,
|
|
210
|
-
a.engine.maxInputTokens ? Number(a.engine.maxInputTokens).toLocaleString() + ' tok/slot' : null,
|
|
211
|
-
a.engine.slots ? a.engine.slots + (a.engine.slots === 1 ? ' slot' : ' slots') : null,
|
|
212
|
-
a.engine.dimensions ? a.engine.dimensions + ' dims' : null]
|
|
213
|
-
.filter(Boolean).join(' · ') %>
|
|
214
|
-
</div>
|
|
215
|
-
<%# WHICH FLAG THIS ENGINE GETS, on an engine already being relied on — not only on
|
|
216
|
-
one somebody might add. Getting it wrong is silent: the model spends its whole
|
|
217
|
-
budget thinking and returns nothing, with no error at all, and this row is the
|
|
218
|
-
only place anybody would look to find out which flag is being sent. %>
|
|
219
|
-
<% if (a.engine.reasoning) { %>
|
|
220
|
-
<div class="small text-body-secondary mt-1">reasoning: <code><%= a.engine.reasoning %></code></div>
|
|
221
|
-
<% } else if (a.engine.role === 'chat') { %>
|
|
222
|
-
<div class="small text-warning mt-1">no known reasoning flag for this model family — neither will be sent</div>
|
|
223
|
-
<% } %>
|
|
224
|
-
<% } %>
|
|
225
|
-
<%# WHAT STILL POINTS AT IT. A missing engine whose jobs nobody has repointed is
|
|
226
|
-
an endpoint that will fail every call — the same silent-but-configured shape
|
|
227
|
-
as the credential problem, so it names the jobs rather than only itself. %>
|
|
228
|
-
<div class="small mt-1 <%= a.missing ? 'text-danger fw-semibold' : 'text-body-secondary' %>">
|
|
229
|
-
<span class="font-monospace"><%= a.row.id %></span>
|
|
230
|
-
<% if (a.routes.length) { %>
|
|
231
|
-
· <%= a.routes.map(function (x) { return _label(x.id); }).join(', ') %>
|
|
232
|
-
<% } else { %>
|
|
233
|
-
· serves no jobs
|
|
234
|
-
<% } %>
|
|
235
|
-
</div>
|
|
236
|
-
<% if (a.missing) { %>
|
|
237
|
-
<div class="small text-danger mt-1">
|
|
238
|
-
The stack no longer lists this engine. Either point its jobs elsewhere, or remove the endpoint.
|
|
239
|
-
</div>
|
|
240
|
-
<% } %>
|
|
241
|
-
</div>
|
|
242
|
-
<div class="d-flex gap-2">
|
|
243
|
-
<%# TEST THIS ENGINE, not whatever is currently serving. An engine you have adopted is an
|
|
244
|
-
endpoint like any other, and the reason to test one is precisely that it is NOT the
|
|
245
|
-
one answering right now — verifying a fallback before it is needed is the point of
|
|
246
|
-
having one.
|
|
247
|
-
|
|
248
|
-
THERE IS NO "SETTINGS" HERE, and that is deliberate. It used to link to the generic
|
|
249
|
-
endpoint form, which knows nothing about lmx: of the eight fields it offers, an
|
|
250
|
-
address and a model are DISCOVERED for these engines and a context size is
|
|
251
|
-
overridden by what the supervisor reports — so five of them do nothing, and two of
|
|
252
|
-
those look exactly like the settings that matter. A control that leads somewhere
|
|
253
|
-
describing a stored address, for a thing whose whole design is that it stores no
|
|
254
|
-
address, is worse than no control.
|
|
255
|
-
|
|
256
|
-
The genuinely per-engine settings — a token ceiling, a timeout, its own key — are
|
|
257
|
-
real and will come back as an inline form on this row, like everything else on this
|
|
258
|
-
screen. Until they do, an engine is removed and re-added rather than edited. %>
|
|
259
|
-
<form method="POST" action="<%= _base %>/<%= encodeURIComponent(a.row.id) %>/test">
|
|
260
|
-
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
261
|
-
<button type="submit" class="btn btn-sm btn-outline-secondary">Test</button>
|
|
262
|
-
</form>
|
|
263
|
-
<%# REMOVE IS THE INVERSE OF ADD, and the panel has had Add since the beginning. Without
|
|
264
|
-
it an adopted engine could be created here and unmade nowhere: lmx rows are excluded
|
|
265
|
-
from the direct-endpoint list on purpose, so this panel is the only place they
|
|
266
|
-
appear at all. A missing engine's row even says "remove the endpoint" — and until
|
|
267
|
-
now offered nothing to press. %>
|
|
268
|
-
<form method="POST" action="<%= _base %>/<%= encodeURIComponent(a.row.id) %>/delete"
|
|
269
|
-
data-confirm="Remove the “<%= a.row.id %>” 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.<% } %>">
|
|
270
|
-
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
271
|
-
<button type="submit" class="btn btn-sm btn-outline-danger">Remove</button>
|
|
272
|
-
</form>
|
|
273
|
-
</div>
|
|
274
|
-
</div>
|
|
275
|
-
<% }); %>
|
|
276
|
-
<% } %>
|
|
277
|
-
|
|
278
|
-
<%# ── ENGINES AVAILABLE ── %>
|
|
279
|
-
<% if (inst.available && inst.available.length) { %>
|
|
280
|
-
<div class="px-3 py-2 bg-body-tertiary d-flex justify-content-between small text-body-secondary border-top">
|
|
281
|
-
<span class="text-uppercase fw-semibold" style="letter-spacing:.08em">Also on this stack — not in use</span>
|
|
282
|
-
<span class="text-uppercase fw-semibold" style="letter-spacing:.08em"><%= inst.available.length %></span>
|
|
283
|
-
</div>
|
|
284
|
-
<% inst.available.forEach(function (e) { %>
|
|
285
|
-
<div class="d-flex justify-content-between align-items-start gap-3 border-top p-3">
|
|
286
|
-
<div>
|
|
287
|
-
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
288
|
-
<strong><%= e.label || e.name %></strong>
|
|
289
|
-
<code class="small"><%= e.name %></code>
|
|
290
|
-
<span class="<%= _pill %> <%= e.state === 'healthy' ? _tone.ok : _tone.warn %>"><%- _dot %> <%= e.state || 'unknown' %></span>
|
|
291
|
-
<span class="<%= _role %>"><%= e.role || '?' %></span>
|
|
292
|
-
</div>
|
|
293
|
-
<div class="small text-body-tertiary font-monospace mt-1">
|
|
294
|
-
<%= [e.modelName || e.model, e.quantisation, e.device,
|
|
295
|
-
e.maxInputTokens ? Number(e.maxInputTokens).toLocaleString() + ' tok/slot' : null,
|
|
296
|
-
e.slots ? e.slots + (e.slots === 1 ? ' slot' : ' slots') : null,
|
|
297
|
-
e.dimensions ? e.dimensions + ' dims' : null].filter(Boolean).join(' · ') %>
|
|
298
|
-
</div>
|
|
299
|
-
<%# WHICH REASONING FLAG THIS MODEL NEEDS. Getting it wrong is silent: the model
|
|
300
|
-
spends its whole budget thinking and returns nothing, with no error at all. %>
|
|
301
|
-
<% if (e.reasoning) { %>
|
|
302
|
-
<div class="small text-body-secondary mt-1">reasoning: <code><%= e.reasoning %></code></div>
|
|
303
|
-
<% } else if (e.role === 'chat') { %>
|
|
304
|
-
<div class="small text-warning mt-1">no known reasoning flag for this model family — neither will be sent</div>
|
|
305
|
-
<% } %>
|
|
306
|
-
</div>
|
|
307
|
-
<% if (_edit) { %>
|
|
308
|
-
<form method="POST" action="<%= _base %>/lmx/<%= inst.id %>/adopt">
|
|
309
|
-
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
310
|
-
<input type="hidden" name="engine" value="<%= e.name %>">
|
|
311
|
-
<% if (e.id) { %><input type="hidden" name="engine_id" value="<%= e.id %>"><% } %>
|
|
312
|
-
<input type="hidden" name="label" value="<%= e.label || e.name %>">
|
|
313
|
-
<input type="hidden" name="role" value="<%= e.role || '' %>">
|
|
314
|
-
<input type="hidden" name="max_input_tokens" value="<%= e.maxInputTokens || '' %>">
|
|
315
|
-
<button type="submit" class="btn btn-sm btn-primary">Add</button>
|
|
316
|
-
</form>
|
|
317
|
-
<% } %>
|
|
318
|
-
</div>
|
|
319
|
-
<% }); %>
|
|
320
|
-
<% } %>
|
|
321
|
-
|
|
322
|
-
</div>
|
|
323
|
-
</div>
|
|
324
|
-
<% } %>
|
|
325
|
-
|
|
326
|
-
<%# ── EDITING, IN PLACE ──────────────────────────────────────────────────────────────────────
|
|
327
|
-
NOT A FORM AT THE FOOT OF THE PAGE. It was one, and pressing Edit scrolled you away from the
|
|
328
|
-
stack you were looking at, down to a form that also served as "Add" — so the thing being
|
|
329
|
-
changed was off screen while you changed it, and nothing on it said which stack you were in.
|
|
330
|
-
|
|
331
|
-
Same two-mode shape as the job card: the panel becomes the form, Save posts, and the redirect
|
|
332
|
-
brings the panel back. %>
|
|
333
|
-
<% if (_edit) { %>
|
|
334
|
-
<form method="POST" action="<%= _base %>/lmx" class="p-3" data-stack-edit<% if (!_isNew) { %> hidden<% } %>>
|
|
335
|
-
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
336
|
-
|
|
337
|
-
<div class="d-flex justify-content-between align-items-baseline mb-3">
|
|
338
|
-
<strong><%= _isNew ? 'Add a supervised stack' : 'Change ' + (inst.label || inst.id) %></strong>
|
|
339
|
-
<span class="small text-body-tertiary">blank secrets keep what is stored</span>
|
|
340
|
-
</div>
|
|
341
|
-
|
|
342
|
-
<div class="row g-3">
|
|
343
|
-
<div class="col-md-4">
|
|
344
|
-
<label class="form-label" for="lmx-id-<%= inst.id %>">Instance</label>
|
|
345
|
-
<%# THE STACK'S OWN NAME, checked against what the status document reports — which is what
|
|
346
|
-
catches a URL pointed at the wrong deployment, since engine names collide across
|
|
347
|
-
stacks. Readonly once it exists: every engine row references it, and it is the value
|
|
348
|
-
the identity check compares against. %>
|
|
349
|
-
<input id="lmx-id-<%= inst.id %>" type="text" class="form-control font-monospace"
|
|
350
|
-
name="id" value="<%= _isNew ? '' : inst.id %>" placeholder="vpc1-dev" required
|
|
351
|
-
<% if (!_isNew) { %>readonly<% } %>>
|
|
352
|
-
<div class="form-text">
|
|
353
|
-
<% if (_isNew) { %>Exactly as the stack reports it. Checked on every poll.
|
|
354
|
-
<% } else { %>Fixed — engines reference it, and the identity check compares against it.<% } %>
|
|
355
|
-
</div>
|
|
356
|
-
</div>
|
|
357
|
-
<div class="col-md-8">
|
|
358
|
-
<label class="form-label" for="lmx-label-<%= inst.id %>">Name</label>
|
|
359
|
-
<input id="lmx-label-<%= inst.id %>" type="text" class="form-control" name="label"
|
|
360
|
-
value="<%= _isNew ? '' : (inst.label || '') %>" placeholder="Dev stack">
|
|
361
|
-
</div>
|
|
362
|
-
<div class="col-12">
|
|
363
|
-
<label class="form-label" for="lmx-url-<%= inst.id %>">Status listener</label>
|
|
364
|
-
<input id="lmx-url-<%= inst.id %>" type="text" class="form-control font-monospace" name="status_url"
|
|
365
|
-
value="<%= _isNew ? '' : (inst.status_url || '') %>"
|
|
366
|
-
placeholder="https://vpc1.example.net:9443/status" required>
|
|
367
|
-
<div class="form-text">Not an engine address — this is only asked where the engines are.</div>
|
|
368
|
-
</div>
|
|
369
|
-
|
|
370
|
-
<%# THE LENGTHS ARE THE POINT. Both are write-only, so the only thing that can be said about
|
|
371
|
-
a stored secret is how long it is. Equal lengths are expected through the gateway, where
|
|
372
|
-
one lmxk_ key is both credentials; direct to lmx they are two different secrets, and
|
|
373
|
-
equal lengths there are worth a second look. The help text says which is which. %>
|
|
374
|
-
<div class="col-md-6">
|
|
375
|
-
<label class="form-label" for="lmx-token-<%= inst.id %>">Status token</label>
|
|
376
|
-
<input id="lmx-token-<%= inst.id %>" type="password" class="form-control" name="status_token"
|
|
377
|
-
autocomplete="new-password"
|
|
378
|
-
placeholder="<%= (inst.creds && inst.creds.statusTokenLength) ? 'unchanged — ' + inst.creds.statusTokenLength + ' characters stored' : 'nothing stored — enter a value' %>">
|
|
379
|
-
<div class="form-text">Reads the status document; never sent to an engine. Direct to lmx: the
|
|
380
|
-
<code>status.token</code>. Through the gateway: your <code>lmxk_</code> key, the same value
|
|
381
|
-
as the engines key.</div>
|
|
382
|
-
</div>
|
|
383
|
-
<div class="col-md-6">
|
|
384
|
-
<label class="form-label" for="lmx-key-<%= inst.id %>">Engines key</label>
|
|
385
|
-
<input id="lmx-key-<%= inst.id %>" type="password" class="form-control" name="engines_key"
|
|
386
|
-
autocomplete="new-password"
|
|
387
|
-
placeholder="<%= (inst.creds && inst.creds.enginesKeyLength) ? 'unchanged — ' + inst.creds.enginesKeyLength + ' characters stored' : 'nothing stored — enter a value' %>">
|
|
388
|
-
<div class="form-text">Used by every engine here unless one overrides it. Through the gateway:
|
|
389
|
-
the same <code>lmxk_</code> key as the status token.</div>
|
|
390
|
-
</div>
|
|
391
|
-
|
|
392
|
-
<div class="col-12">
|
|
393
|
-
<label class="form-label" for="lmx-ca-<%= inst.id %>">Certificate to pin</label>
|
|
394
|
-
<%# PINNED, never "verification off". Turning verification off accepts any certificate from
|
|
395
|
-
anyone able to answer on that address, which is the whole attack pinning prevents. The
|
|
396
|
-
certificate is public, so unlike the tokens it is shown back. %>
|
|
397
|
-
<textarea id="lmx-ca-<%= inst.id %>" class="form-control font-monospace" name="ca_cert" rows="3"
|
|
398
|
-
placeholder="-----BEGIN CERTIFICATE-----"><%= inst.caCert || '' %></textarea>
|
|
399
|
-
<div class="form-text">
|
|
400
|
-
The stack's own certificate. Leave blank if it uses one your system already trusts.
|
|
401
|
-
<% if (!_isNew && inst.pin && inst.pin.valid) { %>
|
|
402
|
-
Pinned now: <span class="font-monospace"><%= inst.pin.fingerprint %></span>,
|
|
403
|
-
expires <%= inst.pin.expires.toISOString().slice(0, 10) %>.
|
|
404
|
-
<% } %>
|
|
405
|
-
</div>
|
|
406
|
-
</div>
|
|
407
|
-
|
|
408
|
-
<div class="col-12">
|
|
409
|
-
<div class="form-check form-switch">
|
|
410
|
-
<input class="form-check-input" type="checkbox" role="switch" name="enabled" value="1"
|
|
411
|
-
id="lmx-enabled-<%= inst.id %>"<% if (_isNew || inst.enabled) { %> checked<% } %>>
|
|
412
|
-
<label class="form-check-label" for="lmx-enabled-<%= inst.id %>">Enabled</label>
|
|
413
|
-
</div>
|
|
414
|
-
</div>
|
|
415
|
-
</div>
|
|
416
|
-
|
|
417
|
-
<div class="d-flex gap-2 mt-3">
|
|
418
|
-
<button type="submit" class="btn btn-sm btn-primary"><%= _isNew ? 'Add stack' : 'Save' %></button>
|
|
419
|
-
<button type="button" class="btn btn-sm btn-outline-secondary" data-stack-cancel>Cancel</button>
|
|
420
|
-
</div>
|
|
421
|
-
<div class="form-text mt-2">
|
|
422
|
-
Every save is checked against the stack, and the per-check result appears on this panel.
|
|
423
|
-
</div>
|
|
424
|
-
</form>
|
|
425
|
-
<% } %>
|
|
426
|
-
</div>
|
|
1
|
+
<%# One supervised stack, as a folding panel.
|
|
2
|
+
────────────────────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
A STACK IS A CONTAINER, not an endpoint. Its address is discovered, its model is a fact the
|
|
4
|
+
supervisor reports, and unfolding it lists engines rather than settings. The header reads
|
|
5
|
+
identity, kind, status, and then A COUNT OF WHAT DEPENDS ON IT — because the question anybody
|
|
6
|
+
has before touching a machine is "if this goes away, what stops working?".
|
|
7
|
+
|
|
8
|
+
FOLDED IS THE DEFAULT ONCE STABLE, so the folded line has to carry the whole verdict: status,
|
|
9
|
+
counts, all three credentials, and when it was last checked. Folding may hide detail; it must
|
|
10
|
+
never hide the reason you would have opened it. The app this was written for previously showed
|
|
11
|
+
a card with name, id and URL — none of which change when you paste a token — so a save that
|
|
12
|
+
worked and one that was discarded were pixel-identical.
|
|
13
|
+
|
|
14
|
+
A PANEL WITH A PROBLEM IS OPEN IN THE SERVER'S HTML, never opened later by script. If attention
|
|
15
|
+
depended on JavaScript, the one state that must never be missed would be the state that
|
|
16
|
+
disappears when a script fails to load.
|
|
17
|
+
|
|
18
|
+
Locals:
|
|
19
|
+
inst a supervisor row, merged with lmxStatus.describeStack() and carrying
|
|
20
|
+
`report` (lmxVerify), `pin` (lmxVerify.readPin), `creds` {statusToken, enginesKey}
|
|
21
|
+
canEdit render the controls at all
|
|
22
|
+
csrfToken () => token, for the forms
|
|
23
|
+
basePath where this app mounts its AI admin, e.g. '/admin/ai'. NOT hardcoded: the routes
|
|
24
|
+
are the consumer's, and only the consumer knows where they live.
|
|
25
|
+
newAnchor where Edit sends somebody to change the stack, e.g. '#lmx-new'
|
|
26
|
+
routeLabel optional (id) => human name for a job; the id is used when absent
|
|
27
|
+
%>
|
|
28
|
+
<%
|
|
29
|
+
// DISTINCT NAMES, deliberately. `var canEdit = (typeof canEdit === "undefined") ? ... : canEdit`
|
|
30
|
+
// reads as a defaulting idiom and is not one: `var` hoists, so inside this template the name is
|
|
31
|
+
// already declared and undefined when the typeof runs. The test is therefore always true, the
|
|
32
|
+
// passed-in local is shadowed, and every consumer silently gets the default.
|
|
33
|
+
var _edit = (typeof canEdit !== 'undefined') && canEdit;
|
|
34
|
+
var _base = (typeof basePath !== 'undefined' && basePath) ? basePath : '/admin/ai';
|
|
35
|
+
var _anchor = (typeof newAnchor !== 'undefined' && newAnchor) ? newAnchor : '#lmx-new';
|
|
36
|
+
var _label = (typeof routeLabel === 'function') ? routeLabel : function (x) { return x; };
|
|
37
|
+
var _csrf = (typeof csrfToken === 'function') ? csrfToken : function () { return ''; };
|
|
38
|
+
var _ago = function (iso) {
|
|
39
|
+
if (!iso) return null;
|
|
40
|
+
var secs = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 1000));
|
|
41
|
+
if (secs < 60) return secs + 's ago';
|
|
42
|
+
if (secs < 3600) return Math.round(secs / 60) + 'm ago';
|
|
43
|
+
return Math.round(secs / 3600) + 'h ago';
|
|
44
|
+
};
|
|
45
|
+
var _glyph = { pass: '✔', fail: '✖', warn: '⚠', skip: '—' };
|
|
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;
|
|
73
|
+
var r = inst.report;
|
|
74
|
+
var _open = !!inst.attention;
|
|
75
|
+
// NOT CHECKED is its own state, distinct from both answering and broken. A stack nobody has asked
|
|
76
|
+
// since the last restart is not a stack in trouble, and colouring it as one would train an
|
|
77
|
+
// operator to ignore the colour.
|
|
78
|
+
var pillCls = !r ? _tone.mute
|
|
79
|
+
: (r.ok ? _tone.ok : (r.level === 'bad' ? _tone.bad : _tone.warn));
|
|
80
|
+
var pillText = !r ? 'not checked' : (r.ok ? 'answering' : 'needs attention');
|
|
81
|
+
%>
|
|
82
|
+
<div class="card mb-3 <%= _open ? 'border-warning' : '' %>" data-panel="lmx-<%= inst.id %>"<% if (_open) { %> data-panel-attention<% } %>
|
|
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 enough for an operator to see
|
|
88
|
+
whether the two match: they SHOULD through the gateway (one lmxk_ key in both fields) and
|
|
89
|
+
should NOT direct to lmx (status.token and an engine key are different credentials). %>
|
|
90
|
+
data-lmx-cert="<%= inst.caCert || '' %>"
|
|
91
|
+
data-lmx-token-len="<%= (inst.creds && inst.creds.statusTokenLength) || '' %>"
|
|
92
|
+
data-lmx-key-len="<%= (inst.creds && inst.creds.enginesKeyLength) || '' %>"
|
|
93
|
+
data-stack="<%= inst.id %>"<% if (_isNew) { %> data-stack-new hidden<% } %>>
|
|
94
|
+
<% if (!_isNew) { %>
|
|
95
|
+
<div data-stack-view>
|
|
96
|
+
<%# THE TOGGLE AND THE ACTIONS ARE SIBLINGS, not nested. A <button> may not contain a link
|
|
97
|
+
or another button — the markup parses unpredictably and the inner control stops being
|
|
98
|
+
reachable by keyboard, which would put Check and Edit behind a mouse. %>
|
|
99
|
+
<div class="d-flex gap-2 align-items-start p-3">
|
|
100
|
+
<button type="button" class="btn text-start border-0 p-0 flex-grow-1 d-flex gap-2 align-items-start"
|
|
101
|
+
style="min-width:0"
|
|
102
|
+
data-panel-toggle="lmx-<%= inst.id %>" aria-expanded="<%= _open ? 'true' : 'false' %>"
|
|
103
|
+
aria-controls="body-lmx-<%= inst.id %>">
|
|
104
|
+
<span class="panel-chev text-body-secondary" aria-hidden="true">›</span>
|
|
105
|
+
<span class="flex-grow-1">
|
|
106
|
+
<span class="d-flex align-items-center gap-2 flex-wrap">
|
|
107
|
+
<strong><%= inst.label || inst.id %></strong>
|
|
108
|
+
<code class="small"><%= inst.id %></code>
|
|
109
|
+
<span class="<%= _role %>">lmx</span>
|
|
110
|
+
<span class="<%= _pill %> <%= pillCls %>"><%- _dot %> <%= pillText %></span>
|
|
111
|
+
<%# THE COUNT IS "WHAT AM I RELYING ON", not what exists. Active means added, enabled
|
|
112
|
+
and healthy in the stack's own document — the number that was missing when a
|
|
113
|
+
configured-but-broken engine looked exactly like a working one. %>
|
|
114
|
+
<span class="<%= _count %>">
|
|
115
|
+
<% if (r && r.engines) { %>
|
|
116
|
+
<%= inst.activeCount %> engine<%= inst.activeCount === 1 ? '' : 's' %> active · <%= inst.reportedCount %> reported<% if (inst.missingCount) { %> · <span class="text-danger"><%= inst.missingCount %> missing</span><% } %>
|
|
117
|
+
<% } else if (r) { %>
|
|
118
|
+
<%# CHECKED, BUT THE LIST DID NOT ARRIVE. "not checked yet" here would be a lie
|
|
119
|
+
about something that just happened 0 seconds ago, and would send somebody to
|
|
120
|
+
press Check again rather than read the reason sitting directly below. %>
|
|
121
|
+
<%= inst.added.length %> added · engines could not be listed
|
|
122
|
+
<% } else { %>
|
|
123
|
+
<%= inst.added.length %> added · not checked yet
|
|
124
|
+
<% } %>
|
|
125
|
+
</span>
|
|
126
|
+
</span>
|
|
127
|
+
<span class="d-block small text-body-secondary font-monospace mt-1" style="word-break:break-all">
|
|
128
|
+
<%= inst.status_url %>
|
|
129
|
+
· token <%= inst.creds.statusToken ? '✔' : '✖' %>
|
|
130
|
+
· key <%= inst.creds.enginesKey ? '✔' : '✖' %>
|
|
131
|
+
· 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 { %>✔<% } %>
|
|
132
|
+
<% if (r) { %>· checked <%= _ago(r.at) %><% } %>
|
|
133
|
+
<% if (!inst.enabled) { %>· <span class="<%= _pill %> <%= _tone.mute %>">disabled</span><% } %>
|
|
134
|
+
</span>
|
|
135
|
+
</span>
|
|
136
|
+
</button>
|
|
137
|
+
<% if (_edit) { %>
|
|
138
|
+
<span class="d-flex gap-2 align-items-center flex-shrink-0">
|
|
139
|
+
<form method="POST" action="<%= _base %>/lmx/<%= inst.id %>/check">
|
|
140
|
+
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
141
|
+
<button type="submit" class="btn btn-sm btn-outline-secondary">Refresh</button>
|
|
142
|
+
</form>
|
|
143
|
+
<%# EVERY ACTION FOR THIS STACK IN ONE PLACE. There used to be a second Refresh at the foot
|
|
144
|
+
of the panel, called "Check now" — the same POST to the same route, so not a second
|
|
145
|
+
control at all, just a second place to look for the first one. And Remove sat down there
|
|
146
|
+
alone, which put the most destructive action furthest from the thing it destroys. %>
|
|
147
|
+
<button type="button" class="btn btn-sm btn-outline-secondary"
|
|
148
|
+
data-stack-edit-open="<%= inst.id %>">Edit</button>
|
|
149
|
+
<form method="POST" action="<%= _base %>/lmx/<%= inst.id %>/delete"
|
|
150
|
+
data-confirm="Remove the “<%= inst.label || inst.id %>” supervisor?">
|
|
151
|
+
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
152
|
+
<button type="submit" class="btn btn-sm btn-outline-danger">Remove</button>
|
|
153
|
+
</form>
|
|
154
|
+
</span>
|
|
155
|
+
<% } %>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div id="body-lmx-<%= inst.id %>" class="border-top"<% if (!_open) { %> hidden<% } %>>
|
|
159
|
+
|
|
160
|
+
<%# THE LAST VERIFICATION, per check. Never a single line: "401" named neither of the two
|
|
161
|
+
credentials it could have been, which is exactly how an engines key pasted into the
|
|
162
|
+
status token's field stayed hidden. %>
|
|
163
|
+
<% if (r) { %>
|
|
164
|
+
<div class="p-3 border-bottom">
|
|
165
|
+
<div class="small text-uppercase fw-semibold text-body-secondary mb-2" style="letter-spacing:.08em">
|
|
166
|
+
Last checked <%= _ago(r.at) %>
|
|
167
|
+
</div>
|
|
168
|
+
<% r.checks.forEach(function (c) { %>
|
|
169
|
+
<div class="d-flex gap-2 small font-monospace">
|
|
170
|
+
<span class="<%= _gcls[c.status] || '' %>" style="width:1rem"><%= _glyph[c.status] || '' %></span>
|
|
171
|
+
<span class="<%= c.status === 'skip' ? 'text-body-tertiary' : '' %>"><%= c.text %></span>
|
|
172
|
+
</div>
|
|
173
|
+
<% }); %>
|
|
174
|
+
</div>
|
|
175
|
+
<% } else { %>
|
|
176
|
+
<div class="p-3 border-bottom small text-body-secondary">
|
|
177
|
+
This stack has not been checked since the last restart.
|
|
178
|
+
<%# NOT PROBED ON RENDER, deliberately: opening a settings page is not evidence that
|
|
179
|
+
anything changed, and a sleeping machine would hold the whole page while it timed
|
|
180
|
+
out. Check asks, when somebody wants to know. %>
|
|
181
|
+
Press <strong>Refresh</strong> to ask it now.
|
|
182
|
+
</div>
|
|
183
|
+
<% } %>
|
|
184
|
+
|
|
185
|
+
<%# ── ENGINES IN USE ── %>
|
|
186
|
+
<% if (inst.added.length) { %>
|
|
187
|
+
<div class="px-3 py-2 bg-body-tertiary d-flex justify-content-between small text-body-secondary">
|
|
188
|
+
<span class="text-uppercase fw-semibold" style="letter-spacing:.08em">In use — <%= inst.added.length %> engine<%= inst.added.length === 1 ? "" : "s" %></span>
|
|
189
|
+
<span class="text-uppercase fw-semibold" style="letter-spacing:.08em">endpoint · jobs served</span>
|
|
190
|
+
</div>
|
|
191
|
+
<% inst.added.forEach(function (a) { %>
|
|
192
|
+
<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 %>"<% } %>>
|
|
193
|
+
<div>
|
|
194
|
+
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
195
|
+
<strong><%= a.row.lmx_engine %></strong>
|
|
196
|
+
<% if (a.missing) { %>
|
|
197
|
+
<span class="<%= _pill %> <%= _tone.bad %>"><%- _dot %> not reported</span>
|
|
198
|
+
<% } else if (a.engine) { %>
|
|
199
|
+
<span class="<%= _pill %> <%= a.engine.state === 'healthy' ? _tone.ok : _tone.warn %>"><%- _dot %> <%= a.engine.state %></span>
|
|
200
|
+
<% if (a.engine.role) { %><span class="<%= _role %>"><%= a.engine.role %></span><% } %>
|
|
201
|
+
<% } %>
|
|
202
|
+
<% if (!Number(a.row.enabled)) { %><span class="<%= _pill %> <%= _tone.mute %>">disabled</span><% } %>
|
|
203
|
+
</div>
|
|
204
|
+
<% if (a.engine) { %>
|
|
205
|
+
<div class="small text-body-tertiary font-monospace mt-1">
|
|
206
|
+
<%= [a.engine.modelName || a.engine.model, a.engine.quantisation,
|
|
207
|
+
a.engine.parameters ? (a.engine.parameters / 1e9).toFixed(1).replace(/\.0$/, '') + 'B' : null,
|
|
208
|
+
a.engine.device,
|
|
209
|
+
a.engine.deviceTotalMiB ? Math.round(a.engine.deviceTotalMiB / 1024) + ' GiB' : null,
|
|
210
|
+
a.engine.maxInputTokens ? Number(a.engine.maxInputTokens).toLocaleString() + ' tok/slot' : null,
|
|
211
|
+
a.engine.slots ? a.engine.slots + (a.engine.slots === 1 ? ' slot' : ' slots') : null,
|
|
212
|
+
a.engine.dimensions ? a.engine.dimensions + ' dims' : null]
|
|
213
|
+
.filter(Boolean).join(' · ') %>
|
|
214
|
+
</div>
|
|
215
|
+
<%# WHICH FLAG THIS ENGINE GETS, on an engine already being relied on — not only on
|
|
216
|
+
one somebody might add. Getting it wrong is silent: the model spends its whole
|
|
217
|
+
budget thinking and returns nothing, with no error at all, and this row is the
|
|
218
|
+
only place anybody would look to find out which flag is being sent. %>
|
|
219
|
+
<% if (a.engine.reasoning) { %>
|
|
220
|
+
<div class="small text-body-secondary mt-1">reasoning: <code><%= a.engine.reasoning %></code></div>
|
|
221
|
+
<% } else if (a.engine.role === 'chat') { %>
|
|
222
|
+
<div class="small text-warning mt-1">no known reasoning flag for this model family — neither will be sent</div>
|
|
223
|
+
<% } %>
|
|
224
|
+
<% } %>
|
|
225
|
+
<%# WHAT STILL POINTS AT IT. A missing engine whose jobs nobody has repointed is
|
|
226
|
+
an endpoint that will fail every call — the same silent-but-configured shape
|
|
227
|
+
as the credential problem, so it names the jobs rather than only itself. %>
|
|
228
|
+
<div class="small mt-1 <%= a.missing ? 'text-danger fw-semibold' : 'text-body-secondary' %>">
|
|
229
|
+
<span class="font-monospace"><%= a.row.id %></span>
|
|
230
|
+
<% if (a.routes.length) { %>
|
|
231
|
+
· <%= a.routes.map(function (x) { return _label(x.id); }).join(', ') %>
|
|
232
|
+
<% } else { %>
|
|
233
|
+
· serves no jobs
|
|
234
|
+
<% } %>
|
|
235
|
+
</div>
|
|
236
|
+
<% if (a.missing) { %>
|
|
237
|
+
<div class="small text-danger mt-1">
|
|
238
|
+
The stack no longer lists this engine. Either point its jobs elsewhere, or remove the endpoint.
|
|
239
|
+
</div>
|
|
240
|
+
<% } %>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="d-flex gap-2">
|
|
243
|
+
<%# TEST THIS ENGINE, not whatever is currently serving. An engine you have adopted is an
|
|
244
|
+
endpoint like any other, and the reason to test one is precisely that it is NOT the
|
|
245
|
+
one answering right now — verifying a fallback before it is needed is the point of
|
|
246
|
+
having one.
|
|
247
|
+
|
|
248
|
+
THERE IS NO "SETTINGS" HERE, and that is deliberate. It used to link to the generic
|
|
249
|
+
endpoint form, which knows nothing about lmx: of the eight fields it offers, an
|
|
250
|
+
address and a model are DISCOVERED for these engines and a context size is
|
|
251
|
+
overridden by what the supervisor reports — so five of them do nothing, and two of
|
|
252
|
+
those look exactly like the settings that matter. A control that leads somewhere
|
|
253
|
+
describing a stored address, for a thing whose whole design is that it stores no
|
|
254
|
+
address, is worse than no control.
|
|
255
|
+
|
|
256
|
+
The genuinely per-engine settings — a token ceiling, a timeout, its own key — are
|
|
257
|
+
real and will come back as an inline form on this row, like everything else on this
|
|
258
|
+
screen. Until they do, an engine is removed and re-added rather than edited. %>
|
|
259
|
+
<form method="POST" action="<%= _base %>/<%= encodeURIComponent(a.row.id) %>/test">
|
|
260
|
+
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
261
|
+
<button type="submit" class="btn btn-sm btn-outline-secondary">Test</button>
|
|
262
|
+
</form>
|
|
263
|
+
<%# REMOVE IS THE INVERSE OF ADD, and the panel has had Add since the beginning. Without
|
|
264
|
+
it an adopted engine could be created here and unmade nowhere: lmx rows are excluded
|
|
265
|
+
from the direct-endpoint list on purpose, so this panel is the only place they
|
|
266
|
+
appear at all. A missing engine's row even says "remove the endpoint" — and until
|
|
267
|
+
now offered nothing to press. %>
|
|
268
|
+
<form method="POST" action="<%= _base %>/<%= encodeURIComponent(a.row.id) %>/delete"
|
|
269
|
+
data-confirm="Remove the “<%= a.row.id %>” 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.<% } %>">
|
|
270
|
+
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
271
|
+
<button type="submit" class="btn btn-sm btn-outline-danger">Remove</button>
|
|
272
|
+
</form>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
<% }); %>
|
|
276
|
+
<% } %>
|
|
277
|
+
|
|
278
|
+
<%# ── ENGINES AVAILABLE ── %>
|
|
279
|
+
<% if (inst.available && inst.available.length) { %>
|
|
280
|
+
<div class="px-3 py-2 bg-body-tertiary d-flex justify-content-between small text-body-secondary border-top">
|
|
281
|
+
<span class="text-uppercase fw-semibold" style="letter-spacing:.08em">Also on this stack — not in use</span>
|
|
282
|
+
<span class="text-uppercase fw-semibold" style="letter-spacing:.08em"><%= inst.available.length %></span>
|
|
283
|
+
</div>
|
|
284
|
+
<% inst.available.forEach(function (e) { %>
|
|
285
|
+
<div class="d-flex justify-content-between align-items-start gap-3 border-top p-3">
|
|
286
|
+
<div>
|
|
287
|
+
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
288
|
+
<strong><%= e.label || e.name %></strong>
|
|
289
|
+
<code class="small"><%= e.name %></code>
|
|
290
|
+
<span class="<%= _pill %> <%= e.state === 'healthy' ? _tone.ok : _tone.warn %>"><%- _dot %> <%= e.state || 'unknown' %></span>
|
|
291
|
+
<span class="<%= _role %>"><%= e.role || '?' %></span>
|
|
292
|
+
</div>
|
|
293
|
+
<div class="small text-body-tertiary font-monospace mt-1">
|
|
294
|
+
<%= [e.modelName || e.model, e.quantisation, e.device,
|
|
295
|
+
e.maxInputTokens ? Number(e.maxInputTokens).toLocaleString() + ' tok/slot' : null,
|
|
296
|
+
e.slots ? e.slots + (e.slots === 1 ? ' slot' : ' slots') : null,
|
|
297
|
+
e.dimensions ? e.dimensions + ' dims' : null].filter(Boolean).join(' · ') %>
|
|
298
|
+
</div>
|
|
299
|
+
<%# WHICH REASONING FLAG THIS MODEL NEEDS. Getting it wrong is silent: the model
|
|
300
|
+
spends its whole budget thinking and returns nothing, with no error at all. %>
|
|
301
|
+
<% if (e.reasoning) { %>
|
|
302
|
+
<div class="small text-body-secondary mt-1">reasoning: <code><%= e.reasoning %></code></div>
|
|
303
|
+
<% } else if (e.role === 'chat') { %>
|
|
304
|
+
<div class="small text-warning mt-1">no known reasoning flag for this model family — neither will be sent</div>
|
|
305
|
+
<% } %>
|
|
306
|
+
</div>
|
|
307
|
+
<% if (_edit) { %>
|
|
308
|
+
<form method="POST" action="<%= _base %>/lmx/<%= inst.id %>/adopt">
|
|
309
|
+
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
310
|
+
<input type="hidden" name="engine" value="<%= e.name %>">
|
|
311
|
+
<% if (e.id) { %><input type="hidden" name="engine_id" value="<%= e.id %>"><% } %>
|
|
312
|
+
<input type="hidden" name="label" value="<%= e.label || e.name %>">
|
|
313
|
+
<input type="hidden" name="role" value="<%= e.role || '' %>">
|
|
314
|
+
<input type="hidden" name="max_input_tokens" value="<%= e.maxInputTokens || '' %>">
|
|
315
|
+
<button type="submit" class="btn btn-sm btn-primary">Add</button>
|
|
316
|
+
</form>
|
|
317
|
+
<% } %>
|
|
318
|
+
</div>
|
|
319
|
+
<% }); %>
|
|
320
|
+
<% } %>
|
|
321
|
+
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
<% } %>
|
|
325
|
+
|
|
326
|
+
<%# ── EDITING, IN PLACE ──────────────────────────────────────────────────────────────────────
|
|
327
|
+
NOT A FORM AT THE FOOT OF THE PAGE. It was one, and pressing Edit scrolled you away from the
|
|
328
|
+
stack you were looking at, down to a form that also served as "Add" — so the thing being
|
|
329
|
+
changed was off screen while you changed it, and nothing on it said which stack you were in.
|
|
330
|
+
|
|
331
|
+
Same two-mode shape as the job card: the panel becomes the form, Save posts, and the redirect
|
|
332
|
+
brings the panel back. %>
|
|
333
|
+
<% if (_edit) { %>
|
|
334
|
+
<form method="POST" action="<%= _base %>/lmx" class="p-3" data-stack-edit<% if (!_isNew) { %> hidden<% } %>>
|
|
335
|
+
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
336
|
+
|
|
337
|
+
<div class="d-flex justify-content-between align-items-baseline mb-3">
|
|
338
|
+
<strong><%= _isNew ? 'Add a supervised stack' : 'Change ' + (inst.label || inst.id) %></strong>
|
|
339
|
+
<span class="small text-body-tertiary">blank secrets keep what is stored</span>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<div class="row g-3">
|
|
343
|
+
<div class="col-md-4">
|
|
344
|
+
<label class="form-label" for="lmx-id-<%= inst.id %>">Instance</label>
|
|
345
|
+
<%# THE STACK'S OWN NAME, checked against what the status document reports — which is what
|
|
346
|
+
catches a URL pointed at the wrong deployment, since engine names collide across
|
|
347
|
+
stacks. Readonly once it exists: every engine row references it, and it is the value
|
|
348
|
+
the identity check compares against. %>
|
|
349
|
+
<input id="lmx-id-<%= inst.id %>" type="text" class="form-control font-monospace"
|
|
350
|
+
name="id" value="<%= _isNew ? '' : inst.id %>" placeholder="vpc1-dev" required
|
|
351
|
+
<% if (!_isNew) { %>readonly<% } %>>
|
|
352
|
+
<div class="form-text">
|
|
353
|
+
<% if (_isNew) { %>Exactly as the stack reports it. Checked on every poll.
|
|
354
|
+
<% } else { %>Fixed — engines reference it, and the identity check compares against it.<% } %>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
<div class="col-md-8">
|
|
358
|
+
<label class="form-label" for="lmx-label-<%= inst.id %>">Name</label>
|
|
359
|
+
<input id="lmx-label-<%= inst.id %>" type="text" class="form-control" name="label"
|
|
360
|
+
value="<%= _isNew ? '' : (inst.label || '') %>" placeholder="Dev stack">
|
|
361
|
+
</div>
|
|
362
|
+
<div class="col-12">
|
|
363
|
+
<label class="form-label" for="lmx-url-<%= inst.id %>">Status listener</label>
|
|
364
|
+
<input id="lmx-url-<%= inst.id %>" type="text" class="form-control font-monospace" name="status_url"
|
|
365
|
+
value="<%= _isNew ? '' : (inst.status_url || '') %>"
|
|
366
|
+
placeholder="https://vpc1.example.net:9443/status" required>
|
|
367
|
+
<div class="form-text">Not an engine address — this is only asked where the engines are.</div>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<%# THE LENGTHS ARE THE POINT. Both are write-only, so the only thing that can be said about
|
|
371
|
+
a stored secret is how long it is. Equal lengths are expected through the gateway, where
|
|
372
|
+
one lmxk_ key is both credentials; direct to lmx they are two different secrets, and
|
|
373
|
+
equal lengths there are worth a second look. The help text says which is which. %>
|
|
374
|
+
<div class="col-md-6">
|
|
375
|
+
<label class="form-label" for="lmx-token-<%= inst.id %>">Status token</label>
|
|
376
|
+
<input id="lmx-token-<%= inst.id %>" type="password" class="form-control" name="status_token"
|
|
377
|
+
autocomplete="new-password"
|
|
378
|
+
placeholder="<%= (inst.creds && inst.creds.statusTokenLength) ? 'unchanged — ' + inst.creds.statusTokenLength + ' characters stored' : 'nothing stored — enter a value' %>">
|
|
379
|
+
<div class="form-text">Reads the status document; never sent to an engine. Direct to lmx: the
|
|
380
|
+
<code>status.token</code>. Through the gateway: your <code>lmxk_</code> key, the same value
|
|
381
|
+
as the engines key.</div>
|
|
382
|
+
</div>
|
|
383
|
+
<div class="col-md-6">
|
|
384
|
+
<label class="form-label" for="lmx-key-<%= inst.id %>">Engines key</label>
|
|
385
|
+
<input id="lmx-key-<%= inst.id %>" type="password" class="form-control" name="engines_key"
|
|
386
|
+
autocomplete="new-password"
|
|
387
|
+
placeholder="<%= (inst.creds && inst.creds.enginesKeyLength) ? 'unchanged — ' + inst.creds.enginesKeyLength + ' characters stored' : 'nothing stored — enter a value' %>">
|
|
388
|
+
<div class="form-text">Used by every engine here unless one overrides it. Through the gateway:
|
|
389
|
+
the same <code>lmxk_</code> key as the status token.</div>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<div class="col-12">
|
|
393
|
+
<label class="form-label" for="lmx-ca-<%= inst.id %>">Certificate to pin</label>
|
|
394
|
+
<%# PINNED, never "verification off". Turning verification off accepts any certificate from
|
|
395
|
+
anyone able to answer on that address, which is the whole attack pinning prevents. The
|
|
396
|
+
certificate is public, so unlike the tokens it is shown back. %>
|
|
397
|
+
<textarea id="lmx-ca-<%= inst.id %>" class="form-control font-monospace" name="ca_cert" rows="3"
|
|
398
|
+
placeholder="-----BEGIN CERTIFICATE-----"><%= inst.caCert || '' %></textarea>
|
|
399
|
+
<div class="form-text">
|
|
400
|
+
The stack's own certificate. Leave blank if it uses one your system already trusts.
|
|
401
|
+
<% if (!_isNew && inst.pin && inst.pin.valid) { %>
|
|
402
|
+
Pinned now: <span class="font-monospace"><%= inst.pin.fingerprint %></span>,
|
|
403
|
+
expires <%= inst.pin.expires.toISOString().slice(0, 10) %>.
|
|
404
|
+
<% } %>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<div class="col-12">
|
|
409
|
+
<div class="form-check form-switch">
|
|
410
|
+
<input class="form-check-input" type="checkbox" role="switch" name="enabled" value="1"
|
|
411
|
+
id="lmx-enabled-<%= inst.id %>"<% if (_isNew || inst.enabled) { %> checked<% } %>>
|
|
412
|
+
<label class="form-check-label" for="lmx-enabled-<%= inst.id %>">Enabled</label>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<div class="d-flex gap-2 mt-3">
|
|
418
|
+
<button type="submit" class="btn btn-sm btn-primary"><%= _isNew ? 'Add stack' : 'Save' %></button>
|
|
419
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" data-stack-cancel>Cancel</button>
|
|
420
|
+
</div>
|
|
421
|
+
<div class="form-text mt-2">
|
|
422
|
+
Every save is checked against the stack, and the per-check result appears on this panel.
|
|
423
|
+
</div>
|
|
424
|
+
</form>
|
|
425
|
+
<% } %>
|
|
426
|
+
</div>
|