@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.
- package/browser/ai-panels.js +131 -113
- package/lmxStatus.js +6 -1
- package/package.json +1 -1
- package/views/ai/lmx-stack.ejs +90 -30
package/browser/ai-panels.js
CHANGED
|
@@ -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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
var
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
set('
|
|
101
|
-
set('
|
|
102
|
-
set('
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "0.15.2",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
7
7
|
"publishConfig": {
|
package/views/ai/lmx-stack.ejs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 ?
|
|
53
|
-
: (r.ok ?
|
|
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="
|
|
71
|
-
<span class="
|
|
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="
|
|
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="
|
|
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">
|
|
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>
|
|
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
|
|
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="
|
|
179
|
+
<span class="<%= _pill %> <%= _tone.bad %>"><%- _dot %> not reported</span>
|
|
150
180
|
<% } else if (a.engine) { %>
|
|
151
|
-
<span class="
|
|
152
|
-
<% if (a.engine.role) { %><span class="
|
|
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="
|
|
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,
|
|
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="
|
|
202
|
-
<span class="
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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) ?
|
|
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<% } %><% } %>
|