@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/job-card.ejs
CHANGED
|
@@ -1,182 +1,182 @@
|
|
|
1
|
-
<%# One job, and the path of endpoints that serves it.
|
|
2
|
-
────────────────────────────────────────────────────────────────────────────────────────────
|
|
3
|
-
TWO MODES, NOT A FOLD. A stack panel folds because its detail is worth reading occasionally; a
|
|
4
|
-
job's detail is a thing you EDIT, and the editing controls are useless the other 99% of the
|
|
5
|
-
time. So the card reads as a sentence — this job, this path, this is what it is for — until
|
|
6
|
-
somebody says they want to change it.
|
|
7
|
-
|
|
8
|
-
THE PATH IS THE HEADLINE, because it is the only part that varies and the only part anybody
|
|
9
|
-
comes here to check. `a → b → c` is the whole configuration of a job in one line: the first
|
|
10
|
-
that answers serves, the rest are its fallbacks in order.
|
|
11
|
-
|
|
12
|
-
ORDER IS CARRIED IN TYPED POSITIONS, never in the order the browser serialises fields. A form
|
|
13
|
-
posts in document order, so a list that relied on that would be silently un-reorderable — and
|
|
14
|
-
the handler this posts to already reads `position_<id>` for exactly that reason. The list is
|
|
15
|
-
renumbered from its own DOM order on submit, so what you see is what is sent.
|
|
16
|
-
|
|
17
|
-
ASSIGNED IS NOT THE SAME AS RESOLVED. A job with no assignment is not unconfigured, it is
|
|
18
|
-
following the default — every enabled endpoint in listed order — and saying so stops somebody
|
|
19
|
-
"fixing" a job that is already doing the right thing. A job whose assigned endpoints are all
|
|
20
|
-
disabled resolves to nothing at all, which is a different and much worse state.
|
|
21
|
-
|
|
22
|
-
Locals:
|
|
23
|
-
job { id, label, character, detail }
|
|
24
|
-
assigned [endpointId] explicitly chosen for this job, in order (may be empty)
|
|
25
|
-
resolved [endpointId] what would actually serve right now, in order
|
|
26
|
-
endpoints [{ id, label, enabled }] every endpoint that exists
|
|
27
|
-
spend { calls, total_tokens } | null
|
|
28
|
-
spendDays how many days `spend` covers, for the label beside it (default 14)
|
|
29
|
-
canEdit render the edit affordance at all
|
|
30
|
-
csrfToken () => token
|
|
31
|
-
basePath where this app mounts its AI admin, e.g. '/admin/ai'
|
|
32
|
-
%>
|
|
33
|
-
<%
|
|
34
|
-
var _edit = (typeof canEdit !== 'undefined') && canEdit;
|
|
35
|
-
var _base = (typeof basePath !== 'undefined' && basePath) ? basePath : '/admin/ai';
|
|
36
|
-
var _csrf = (typeof csrfToken === 'function') ? csrfToken : function () { return ''; };
|
|
37
|
-
var _assigned = assigned || [];
|
|
38
|
-
var _resolved = resolved || [];
|
|
39
|
-
var _eps = endpoints || [];
|
|
40
|
-
var _spend = (typeof spend !== 'undefined') ? spend : null;
|
|
41
|
-
// THE WINDOW THAT PRODUCED `spend`. Defaulted to what the label used to say as a literal, so a
|
|
42
|
-
// consumer that passes nothing renders exactly as it did before.
|
|
43
|
-
var _spendDays = (typeof spendDays !== 'undefined' && Number(spendDays) > 0) ? Number(spendDays) : 14;
|
|
44
|
-
var _isDefault = !_assigned.length;
|
|
45
|
-
var _byId = {};
|
|
46
|
-
_eps.forEach(function (e) { _byId[e.id] = e; });
|
|
47
|
-
// The path being EDITED is the assignment, not the resolution: editing what is currently serving
|
|
48
|
-
// would silently drop a disabled endpoint from the assignment the moment anybody pressed Save.
|
|
49
|
-
var _path = _assigned.slice();
|
|
50
|
-
var _rest = _eps.filter(function (e) { return _path.indexOf(e.id) === -1; });
|
|
51
|
-
%>
|
|
52
|
-
<div class="border rounded mb-2" data-job="<%= job.id %>">
|
|
53
|
-
|
|
54
|
-
<%# ── READING MODE ────────────────────────────────────────────────────────────────────────── %>
|
|
55
|
-
<div class="p-3" data-job-view>
|
|
56
|
-
<%# THE ACTIONS STAY ON THE RIGHT, whatever the description does.
|
|
57
|
-
`flex-wrap` plus a growing child with no `min-width: 0` is the classic way to lose a
|
|
58
|
-
button: a flex item will not shrink below its content's intrinsic width, so a long
|
|
59
|
-
description keeps the text column at full width, the actions wrap onto a line of their own,
|
|
60
|
-
and `justify-content-between` then puts that lone item at the START — bottom LEFT, which is
|
|
61
|
-
precisely where the Edit button went.
|
|
62
|
-
`min-width: 0` lets the text column shrink so its own text wraps instead; `flex-shrink-0`
|
|
63
|
-
keeps the buttons their natural size; and with no wrapping there is no second line to fall
|
|
64
|
-
onto. %>
|
|
65
|
-
<div class="d-flex justify-content-between align-items-start gap-3">
|
|
66
|
-
<div class="flex-grow-1" style="min-width:0">
|
|
67
|
-
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
68
|
-
<strong><%= job.label %></strong>
|
|
69
|
-
<span class="badge border bg-body-tertiary text-body-secondary text-uppercase fw-semibold"><%= job.character %></span>
|
|
70
|
-
<% if (_isDefault) { %>
|
|
71
|
-
<span class="badge rounded-pill border bg-body-secondary text-body-secondary fw-semibold">default</span>
|
|
72
|
-
<% } %>
|
|
73
|
-
</div>
|
|
74
|
-
<%# THE PATH, directly under the name — the one line that says how this job is configured. %>
|
|
75
|
-
<div class="font-monospace small mt-1" style="word-break:break-all">
|
|
76
|
-
<% if (_resolved.length) { %>
|
|
77
|
-
<% _resolved.forEach(function (id, i) { %><% if (i) { %><span class="text-body-tertiary"> → </span><% } %><span class="<%= i ? 'text-body-secondary' : '' %>"><%= id %></span><% }) %>
|
|
78
|
-
<% } else { %>
|
|
79
|
-
<span class="text-danger">nothing available — every endpoint for this job is disabled or missing</span>
|
|
80
|
-
<% } %>
|
|
81
|
-
</div>
|
|
82
|
-
<div class="form-text mt-1 mb-0"><%= job.detail %></div>
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
<div class="text-end d-flex flex-column align-items-end gap-2 flex-shrink-0">
|
|
86
|
-
<%# WHAT THIS JOB COSTS, which is the number worth acting on: a job can be pointed at a
|
|
87
|
-
cheaper machine, run less often, or switched off. An endpoint total only says where the
|
|
88
|
-
money went, never what asked for it.
|
|
89
|
-
|
|
90
|
-
THE WINDOW IS PRINTED FROM THE SAME PLACE THE FIGURE COMES FROM. It used to be the
|
|
91
|
-
literal "14 days" here, while the figure beside it was aggregated by the consuming app
|
|
92
|
-
over a window only that app knew — two halves of one sentence in two repositories. A
|
|
93
|
-
consumer changing its window relabelled nothing, and every card then stated a period
|
|
94
|
-
its own number had never covered. Support101 hit exactly that on the other side of the
|
|
95
|
-
same screen, where a total labelled "14 days" came from an unwindowed query and ran
|
|
96
|
-
139,792 tokens adrift of the tab beside it. %>
|
|
97
|
-
<% if (_spend && _spend.total_tokens) { %>
|
|
98
|
-
<div class="small text-body-secondary" style="font-variant-numeric: tabular-nums">
|
|
99
|
-
<%= Number(_spend.total_tokens).toLocaleString() %> tokens in <%= _spend.calls %> call<%= _spend.calls === 1 ? '' : 's' %>
|
|
100
|
-
<span class="text-body-tertiary">· <%= _spendDays %> days</span>
|
|
101
|
-
</div>
|
|
102
|
-
<% } %>
|
|
103
|
-
<% if (_edit && _eps.length) { %>
|
|
104
|
-
<button type="button" class="btn btn-sm btn-outline-secondary" data-job-edit-open="<%= job.id %>">Edit</button>
|
|
105
|
-
<% } %>
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
|
|
110
|
-
<%# ── EDITING MODE ───────────────────────────────────────────────────────────────────────────
|
|
111
|
-
Rendered by the server and hidden, rather than built by script when Edit is pressed: the
|
|
112
|
-
whole list is here in the markup, so it works from a keyboard, survives a script that fails
|
|
113
|
-
to load badly enough to matter, and posts the same fields the handler has always read. %>
|
|
114
|
-
<% if (_edit && _eps.length) { %>
|
|
115
|
-
<form method="POST" action="<%= _base %>/routes/<%= encodeURIComponent(job.id) %>"
|
|
116
|
-
class="p-3 border-top" data-job-edit hidden>
|
|
117
|
-
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
118
|
-
|
|
119
|
-
<div class="d-flex justify-content-between align-items-baseline mb-2">
|
|
120
|
-
<span class="small text-uppercase fw-semibold text-body-secondary" style="letter-spacing:.08em">
|
|
121
|
-
<%= job.label %> — the path, in order
|
|
122
|
-
</span>
|
|
123
|
-
<span class="small text-body-tertiary">first to answer serves</span>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
<ol class="list-unstyled mb-2" data-job-list>
|
|
127
|
-
<% _path.forEach(function (id, i) { %>
|
|
128
|
-
<% var ep = _byId[id]; %>
|
|
129
|
-
<li class="d-flex align-items-center gap-2 border rounded px-2 py-2 mb-1 bg-body-tertiary"
|
|
130
|
-
draggable="true" data-job-item="<%= id %>">
|
|
131
|
-
<span class="text-body-tertiary" aria-hidden="true" style="cursor:grab">⠿</span>
|
|
132
|
-
<span class="badge rounded-pill border bg-body-secondary text-body-secondary font-monospace fw-normal"
|
|
133
|
-
data-job-rank><%= i + 1 %></span>
|
|
134
|
-
<span class="flex-grow-1 font-monospace small">
|
|
135
|
-
<%= id %>
|
|
136
|
-
<% if (ep && ep.label && ep.label !== id) { %><span class="text-body-tertiary"> · <%= ep.label %></span><% } %>
|
|
137
|
-
<%# AN ENDPOINT IN THE PATH BUT DISABLED IS KEPT, and said out loud. Dropping it on
|
|
138
|
-
save would quietly rewrite somebody's configuration because a machine happened to
|
|
139
|
-
be switched off this afternoon. %>
|
|
140
|
-
<% if (ep && !ep.enabled) { %>
|
|
141
|
-
<span class="badge rounded-pill border bg-warning-subtle text-warning-emphasis border-warning-subtle ms-1">disabled — skipped while off</span>
|
|
142
|
-
<% } %>
|
|
143
|
-
<% if (!ep) { %>
|
|
144
|
-
<span class="badge rounded-pill border bg-danger-subtle text-danger-emphasis border-danger-subtle ms-1">no such endpoint</span>
|
|
145
|
-
<% } %>
|
|
146
|
-
</span>
|
|
147
|
-
<span class="btn-group btn-group-sm" role="group">
|
|
148
|
-
<button type="button" class="btn btn-outline-secondary" data-job-up
|
|
149
|
-
aria-label="Move <%= id %> earlier">↑</button>
|
|
150
|
-
<button type="button" class="btn btn-outline-secondary" data-job-down
|
|
151
|
-
aria-label="Move <%= id %> later">↓</button>
|
|
152
|
-
<button type="button" class="btn btn-outline-secondary" data-job-remove
|
|
153
|
-
aria-label="Remove <%= id %> from this job">✕</button>
|
|
154
|
-
</span>
|
|
155
|
-
</li>
|
|
156
|
-
<% }) %>
|
|
157
|
-
</ol>
|
|
158
|
-
|
|
159
|
-
<p class="form-text mt-0" data-job-empty<% if (_path.length) { %> hidden<% } %>>
|
|
160
|
-
Nothing chosen, so this job uses <strong>every enabled endpoint</strong> in the order they
|
|
161
|
-
are listed above. That is a working configuration, not a missing one.
|
|
162
|
-
</p>
|
|
163
|
-
|
|
164
|
-
<% if (_rest.length) { %>
|
|
165
|
-
<div class="small text-uppercase fw-semibold text-body-secondary mt-3 mb-2" style="letter-spacing:.08em">Not in this job</div>
|
|
166
|
-
<div class="d-flex flex-wrap gap-2" data-job-pool>
|
|
167
|
-
<% _rest.forEach(function (e) { %>
|
|
168
|
-
<button type="button" class="btn btn-sm btn-outline-secondary font-monospace"
|
|
169
|
-
data-job-add="<%= e.id %>"<% if (!e.enabled) { %> disabled title="This endpoint is switched off"<% } %>>
|
|
170
|
-
+ <%= e.id %><% if (!e.enabled) { %> (disabled)<% } %>
|
|
171
|
-
</button>
|
|
172
|
-
<% }) %>
|
|
173
|
-
</div>
|
|
174
|
-
<% } %>
|
|
175
|
-
|
|
176
|
-
<div class="d-flex gap-2 mt-3">
|
|
177
|
-
<button type="submit" class="btn btn-sm btn-primary">Save</button>
|
|
178
|
-
<button type="button" class="btn btn-sm btn-outline-secondary" data-job-cancel>Cancel</button>
|
|
179
|
-
</div>
|
|
180
|
-
</form>
|
|
181
|
-
<% } %>
|
|
182
|
-
</div>
|
|
1
|
+
<%# One job, and the path of endpoints that serves it.
|
|
2
|
+
────────────────────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
TWO MODES, NOT A FOLD. A stack panel folds because its detail is worth reading occasionally; a
|
|
4
|
+
job's detail is a thing you EDIT, and the editing controls are useless the other 99% of the
|
|
5
|
+
time. So the card reads as a sentence — this job, this path, this is what it is for — until
|
|
6
|
+
somebody says they want to change it.
|
|
7
|
+
|
|
8
|
+
THE PATH IS THE HEADLINE, because it is the only part that varies and the only part anybody
|
|
9
|
+
comes here to check. `a → b → c` is the whole configuration of a job in one line: the first
|
|
10
|
+
that answers serves, the rest are its fallbacks in order.
|
|
11
|
+
|
|
12
|
+
ORDER IS CARRIED IN TYPED POSITIONS, never in the order the browser serialises fields. A form
|
|
13
|
+
posts in document order, so a list that relied on that would be silently un-reorderable — and
|
|
14
|
+
the handler this posts to already reads `position_<id>` for exactly that reason. The list is
|
|
15
|
+
renumbered from its own DOM order on submit, so what you see is what is sent.
|
|
16
|
+
|
|
17
|
+
ASSIGNED IS NOT THE SAME AS RESOLVED. A job with no assignment is not unconfigured, it is
|
|
18
|
+
following the default — every enabled endpoint in listed order — and saying so stops somebody
|
|
19
|
+
"fixing" a job that is already doing the right thing. A job whose assigned endpoints are all
|
|
20
|
+
disabled resolves to nothing at all, which is a different and much worse state.
|
|
21
|
+
|
|
22
|
+
Locals:
|
|
23
|
+
job { id, label, character, detail }
|
|
24
|
+
assigned [endpointId] explicitly chosen for this job, in order (may be empty)
|
|
25
|
+
resolved [endpointId] what would actually serve right now, in order
|
|
26
|
+
endpoints [{ id, label, enabled }] every endpoint that exists
|
|
27
|
+
spend { calls, total_tokens } | null
|
|
28
|
+
spendDays how many days `spend` covers, for the label beside it (default 14)
|
|
29
|
+
canEdit render the edit affordance at all
|
|
30
|
+
csrfToken () => token
|
|
31
|
+
basePath where this app mounts its AI admin, e.g. '/admin/ai'
|
|
32
|
+
%>
|
|
33
|
+
<%
|
|
34
|
+
var _edit = (typeof canEdit !== 'undefined') && canEdit;
|
|
35
|
+
var _base = (typeof basePath !== 'undefined' && basePath) ? basePath : '/admin/ai';
|
|
36
|
+
var _csrf = (typeof csrfToken === 'function') ? csrfToken : function () { return ''; };
|
|
37
|
+
var _assigned = assigned || [];
|
|
38
|
+
var _resolved = resolved || [];
|
|
39
|
+
var _eps = endpoints || [];
|
|
40
|
+
var _spend = (typeof spend !== 'undefined') ? spend : null;
|
|
41
|
+
// THE WINDOW THAT PRODUCED `spend`. Defaulted to what the label used to say as a literal, so a
|
|
42
|
+
// consumer that passes nothing renders exactly as it did before.
|
|
43
|
+
var _spendDays = (typeof spendDays !== 'undefined' && Number(spendDays) > 0) ? Number(spendDays) : 14;
|
|
44
|
+
var _isDefault = !_assigned.length;
|
|
45
|
+
var _byId = {};
|
|
46
|
+
_eps.forEach(function (e) { _byId[e.id] = e; });
|
|
47
|
+
// The path being EDITED is the assignment, not the resolution: editing what is currently serving
|
|
48
|
+
// would silently drop a disabled endpoint from the assignment the moment anybody pressed Save.
|
|
49
|
+
var _path = _assigned.slice();
|
|
50
|
+
var _rest = _eps.filter(function (e) { return _path.indexOf(e.id) === -1; });
|
|
51
|
+
%>
|
|
52
|
+
<div class="border rounded mb-2" data-job="<%= job.id %>">
|
|
53
|
+
|
|
54
|
+
<%# ── READING MODE ────────────────────────────────────────────────────────────────────────── %>
|
|
55
|
+
<div class="p-3" data-job-view>
|
|
56
|
+
<%# THE ACTIONS STAY ON THE RIGHT, whatever the description does.
|
|
57
|
+
`flex-wrap` plus a growing child with no `min-width: 0` is the classic way to lose a
|
|
58
|
+
button: a flex item will not shrink below its content's intrinsic width, so a long
|
|
59
|
+
description keeps the text column at full width, the actions wrap onto a line of their own,
|
|
60
|
+
and `justify-content-between` then puts that lone item at the START — bottom LEFT, which is
|
|
61
|
+
precisely where the Edit button went.
|
|
62
|
+
`min-width: 0` lets the text column shrink so its own text wraps instead; `flex-shrink-0`
|
|
63
|
+
keeps the buttons their natural size; and with no wrapping there is no second line to fall
|
|
64
|
+
onto. %>
|
|
65
|
+
<div class="d-flex justify-content-between align-items-start gap-3">
|
|
66
|
+
<div class="flex-grow-1" style="min-width:0">
|
|
67
|
+
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
68
|
+
<strong><%= job.label %></strong>
|
|
69
|
+
<span class="badge border bg-body-tertiary text-body-secondary text-uppercase fw-semibold"><%= job.character %></span>
|
|
70
|
+
<% if (_isDefault) { %>
|
|
71
|
+
<span class="badge rounded-pill border bg-body-secondary text-body-secondary fw-semibold">default</span>
|
|
72
|
+
<% } %>
|
|
73
|
+
</div>
|
|
74
|
+
<%# THE PATH, directly under the name — the one line that says how this job is configured. %>
|
|
75
|
+
<div class="font-monospace small mt-1" style="word-break:break-all">
|
|
76
|
+
<% if (_resolved.length) { %>
|
|
77
|
+
<% _resolved.forEach(function (id, i) { %><% if (i) { %><span class="text-body-tertiary"> → </span><% } %><span class="<%= i ? 'text-body-secondary' : '' %>"><%= id %></span><% }) %>
|
|
78
|
+
<% } else { %>
|
|
79
|
+
<span class="text-danger">nothing available — every endpoint for this job is disabled or missing</span>
|
|
80
|
+
<% } %>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="form-text mt-1 mb-0"><%= job.detail %></div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="text-end d-flex flex-column align-items-end gap-2 flex-shrink-0">
|
|
86
|
+
<%# WHAT THIS JOB COSTS, which is the number worth acting on: a job can be pointed at a
|
|
87
|
+
cheaper machine, run less often, or switched off. An endpoint total only says where the
|
|
88
|
+
money went, never what asked for it.
|
|
89
|
+
|
|
90
|
+
THE WINDOW IS PRINTED FROM THE SAME PLACE THE FIGURE COMES FROM. It used to be the
|
|
91
|
+
literal "14 days" here, while the figure beside it was aggregated by the consuming app
|
|
92
|
+
over a window only that app knew — two halves of one sentence in two repositories. A
|
|
93
|
+
consumer changing its window relabelled nothing, and every card then stated a period
|
|
94
|
+
its own number had never covered. Support101 hit exactly that on the other side of the
|
|
95
|
+
same screen, where a total labelled "14 days" came from an unwindowed query and ran
|
|
96
|
+
139,792 tokens adrift of the tab beside it. %>
|
|
97
|
+
<% if (_spend && _spend.total_tokens) { %>
|
|
98
|
+
<div class="small text-body-secondary" style="font-variant-numeric: tabular-nums">
|
|
99
|
+
<%= Number(_spend.total_tokens).toLocaleString() %> tokens in <%= _spend.calls %> call<%= _spend.calls === 1 ? '' : 's' %>
|
|
100
|
+
<span class="text-body-tertiary">· <%= _spendDays %> days</span>
|
|
101
|
+
</div>
|
|
102
|
+
<% } %>
|
|
103
|
+
<% if (_edit && _eps.length) { %>
|
|
104
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" data-job-edit-open="<%= job.id %>">Edit</button>
|
|
105
|
+
<% } %>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<%# ── EDITING MODE ───────────────────────────────────────────────────────────────────────────
|
|
111
|
+
Rendered by the server and hidden, rather than built by script when Edit is pressed: the
|
|
112
|
+
whole list is here in the markup, so it works from a keyboard, survives a script that fails
|
|
113
|
+
to load badly enough to matter, and posts the same fields the handler has always read. %>
|
|
114
|
+
<% if (_edit && _eps.length) { %>
|
|
115
|
+
<form method="POST" action="<%= _base %>/routes/<%= encodeURIComponent(job.id) %>"
|
|
116
|
+
class="p-3 border-top" data-job-edit hidden>
|
|
117
|
+
<input type="hidden" name="_csrf" value="<%= _csrf() %>">
|
|
118
|
+
|
|
119
|
+
<div class="d-flex justify-content-between align-items-baseline mb-2">
|
|
120
|
+
<span class="small text-uppercase fw-semibold text-body-secondary" style="letter-spacing:.08em">
|
|
121
|
+
<%= job.label %> — the path, in order
|
|
122
|
+
</span>
|
|
123
|
+
<span class="small text-body-tertiary">first to answer serves</span>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<ol class="list-unstyled mb-2" data-job-list>
|
|
127
|
+
<% _path.forEach(function (id, i) { %>
|
|
128
|
+
<% var ep = _byId[id]; %>
|
|
129
|
+
<li class="d-flex align-items-center gap-2 border rounded px-2 py-2 mb-1 bg-body-tertiary"
|
|
130
|
+
draggable="true" data-job-item="<%= id %>">
|
|
131
|
+
<span class="text-body-tertiary" aria-hidden="true" style="cursor:grab">⠿</span>
|
|
132
|
+
<span class="badge rounded-pill border bg-body-secondary text-body-secondary font-monospace fw-normal"
|
|
133
|
+
data-job-rank><%= i + 1 %></span>
|
|
134
|
+
<span class="flex-grow-1 font-monospace small">
|
|
135
|
+
<%= id %>
|
|
136
|
+
<% if (ep && ep.label && ep.label !== id) { %><span class="text-body-tertiary"> · <%= ep.label %></span><% } %>
|
|
137
|
+
<%# AN ENDPOINT IN THE PATH BUT DISABLED IS KEPT, and said out loud. Dropping it on
|
|
138
|
+
save would quietly rewrite somebody's configuration because a machine happened to
|
|
139
|
+
be switched off this afternoon. %>
|
|
140
|
+
<% if (ep && !ep.enabled) { %>
|
|
141
|
+
<span class="badge rounded-pill border bg-warning-subtle text-warning-emphasis border-warning-subtle ms-1">disabled — skipped while off</span>
|
|
142
|
+
<% } %>
|
|
143
|
+
<% if (!ep) { %>
|
|
144
|
+
<span class="badge rounded-pill border bg-danger-subtle text-danger-emphasis border-danger-subtle ms-1">no such endpoint</span>
|
|
145
|
+
<% } %>
|
|
146
|
+
</span>
|
|
147
|
+
<span class="btn-group btn-group-sm" role="group">
|
|
148
|
+
<button type="button" class="btn btn-outline-secondary" data-job-up
|
|
149
|
+
aria-label="Move <%= id %> earlier">↑</button>
|
|
150
|
+
<button type="button" class="btn btn-outline-secondary" data-job-down
|
|
151
|
+
aria-label="Move <%= id %> later">↓</button>
|
|
152
|
+
<button type="button" class="btn btn-outline-secondary" data-job-remove
|
|
153
|
+
aria-label="Remove <%= id %> from this job">✕</button>
|
|
154
|
+
</span>
|
|
155
|
+
</li>
|
|
156
|
+
<% }) %>
|
|
157
|
+
</ol>
|
|
158
|
+
|
|
159
|
+
<p class="form-text mt-0" data-job-empty<% if (_path.length) { %> hidden<% } %>>
|
|
160
|
+
Nothing chosen, so this job uses <strong>every enabled endpoint</strong> in the order they
|
|
161
|
+
are listed above. That is a working configuration, not a missing one.
|
|
162
|
+
</p>
|
|
163
|
+
|
|
164
|
+
<% if (_rest.length) { %>
|
|
165
|
+
<div class="small text-uppercase fw-semibold text-body-secondary mt-3 mb-2" style="letter-spacing:.08em">Not in this job</div>
|
|
166
|
+
<div class="d-flex flex-wrap gap-2" data-job-pool>
|
|
167
|
+
<% _rest.forEach(function (e) { %>
|
|
168
|
+
<button type="button" class="btn btn-sm btn-outline-secondary font-monospace"
|
|
169
|
+
data-job-add="<%= e.id %>"<% if (!e.enabled) { %> disabled title="This endpoint is switched off"<% } %>>
|
|
170
|
+
+ <%= e.id %><% if (!e.enabled) { %> (disabled)<% } %>
|
|
171
|
+
</button>
|
|
172
|
+
<% }) %>
|
|
173
|
+
</div>
|
|
174
|
+
<% } %>
|
|
175
|
+
|
|
176
|
+
<div class="d-flex gap-2 mt-3">
|
|
177
|
+
<button type="submit" class="btn btn-sm btn-primary">Save</button>
|
|
178
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" data-job-cancel>Cancel</button>
|
|
179
|
+
</div>
|
|
180
|
+
</form>
|
|
181
|
+
<% } %>
|
|
182
|
+
</div>
|