@weiping/mbridge-page 0.1.0 → 0.2.0
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/package.json +8 -2
- package/page/app.js +278 -139
- package/page/index.html +54 -22
- package/page/style.css +37 -16
- package/page/view.js +275 -160
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weiping/mbridge-page",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "mbridge's usage at a glance inside magpie: what magpie's ledger recorded, and where mbridge's own status page is.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -33,6 +33,12 @@
|
|
|
33
33
|
"file": "page/index.html",
|
|
34
34
|
"icon": "page/icon.svg"
|
|
35
35
|
},
|
|
36
|
-
"permissions": [
|
|
36
|
+
"permissions": [
|
|
37
|
+
"usage:summary"
|
|
38
|
+
],
|
|
39
|
+
"options": {
|
|
40
|
+
"period": "7d",
|
|
41
|
+
"sort": "tokens"
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
}
|
package/page/app.js
CHANGED
|
@@ -1,161 +1,300 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// app.js reads magpie's ledger and draws it. It never reaches mbridge:
|
|
2
|
+
// the sandbox a magpie page runs in has no network at all (connect-src
|
|
3
|
+
// 'none'), and the only thing this page needs is what magpie already
|
|
4
|
+
// recorded (SP14 §2).
|
|
5
|
+
//
|
|
6
|
+
// A classic script, like the community's usage page: it reads the view
|
|
7
|
+
// from globalThis.
|
|
8
|
+
(async () => {
|
|
9
|
+
const V = globalThis.MbridgeView;
|
|
10
|
+
const $ = (id) => document.getElementById(id);
|
|
11
|
+
// el makes an element with text and class
|
|
12
|
+
function el(tag, attrs, kids) {
|
|
13
|
+
const n = document.createElement(tag);
|
|
14
|
+
for (const [k, v] of Object.entries(attrs || {})) {
|
|
15
|
+
if (v === undefined || v === null) continue;
|
|
16
|
+
if (k === "text") n.textContent = v;
|
|
17
|
+
else if (k === "class") n.className = v;
|
|
18
|
+
else n.setAttribute(k, v);
|
|
19
|
+
}
|
|
20
|
+
for (const kid of [].concat(kids || [])) {
|
|
21
|
+
if (kid === undefined || kid === null) continue;
|
|
22
|
+
n.appendChild(typeof kid === "object" ? kid : document.createTextNode(String(kid)));
|
|
23
|
+
}
|
|
24
|
+
return n;
|
|
25
|
+
}
|
|
26
|
+
const theme = await magpie.ready;
|
|
27
|
+
let lang = theme.lang;
|
|
6
28
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
29
|
+
// the period and the sort live in this plugin's own options, so the
|
|
30
|
+
// page opens where it was left
|
|
31
|
+
let opts = {};
|
|
32
|
+
try {
|
|
33
|
+
opts = (await magpie.options.get()) || {};
|
|
34
|
+
} catch {
|
|
35
|
+
// no options yet: the defaults in package.json stand
|
|
36
|
+
}
|
|
37
|
+
const state = {
|
|
38
|
+
period: V.PERIODS.includes(opts.period) ? opts.period : "7d",
|
|
39
|
+
sort: V.SORTS.includes(opts.sort) ? opts.sort : "tokens",
|
|
40
|
+
summary: null,
|
|
41
|
+
error: null,
|
|
42
|
+
// what mbridge published into the options (SP14 §7)
|
|
43
|
+
mbridge: null,
|
|
44
|
+
// the sequence number of the last command the page sent
|
|
45
|
+
cmdSeq: 0,
|
|
46
|
+
};
|
|
10
47
|
|
|
11
|
-
|
|
48
|
+
function words() {
|
|
49
|
+
const t = V.text(lang);
|
|
50
|
+
document.querySelectorAll("[data-t]").forEach((e) => {
|
|
51
|
+
e.textContent = t[e.dataset.t];
|
|
52
|
+
});
|
|
53
|
+
$("status").textContent = t.status;
|
|
54
|
+
$("restart").textContent = t.restart;
|
|
55
|
+
$("cmd").textContent = "mbridge service restart";
|
|
56
|
+
}
|
|
12
57
|
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
58
|
+
function pick(group, attr, value) {
|
|
59
|
+
$(group).querySelectorAll("button").forEach((b) => {
|
|
60
|
+
b.setAttribute("aria-selected", String(b.dataset[attr] === value));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
16
63
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
64
|
+
// paintMbridge draws what mbridge published: its numbers, the sessions
|
|
65
|
+
// running now, and the decisions it made.
|
|
66
|
+
function paintMbridge() {
|
|
67
|
+
const t = V.text(lang);
|
|
68
|
+
const v = V.mbridgeView(state.mbridge, lang);
|
|
69
|
+
$("mbridge-age").textContent = v.none ? "" : (t.mbridgeAge + " " + v.ageText);
|
|
70
|
+
$("mbridge-empty").textContent = v.none ? v.text : (v.noDecisions ? t.noDecisions : "");
|
|
71
|
+
$("mbridge-empty").hidden = !(v.none || v.noDecisions);
|
|
72
|
+
const stats = $("mbridge-stats");
|
|
73
|
+
stats.textContent = "";
|
|
74
|
+
if (!v.none) {
|
|
75
|
+
const items = [
|
|
76
|
+
[String(v.turnCount), t.mbridgeTurns],
|
|
77
|
+
[v.coverage, t.mbridgeCoverage],
|
|
78
|
+
[String(v.failures), t.mbridgeFailures],
|
|
79
|
+
[v.topReason || "—", t.reason],
|
|
80
|
+
];
|
|
81
|
+
for (const [value, label] of items) {
|
|
82
|
+
const cell = document.createElement("div");
|
|
83
|
+
cell.className = "stat";
|
|
84
|
+
const b = document.createElement("b");
|
|
85
|
+
b.textContent = value;
|
|
86
|
+
const s2 = document.createElement("span");
|
|
87
|
+
s2.textContent = label;
|
|
88
|
+
cell.append(b, s2);
|
|
89
|
+
stats.append(cell);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const live = $("mbridge-live");
|
|
93
|
+
live.textContent = "";
|
|
94
|
+
if (v.none) {
|
|
95
|
+
live.append(el("div", { class: "muted", text: t.mbridgeNone }));
|
|
96
|
+
} else if (v.emptyLive) {
|
|
97
|
+
live.append(el("div", { class: "muted", text: t.mbridgeNoLive }));
|
|
98
|
+
} else {
|
|
99
|
+
for (const c of v.live) {
|
|
100
|
+
const bits = [];
|
|
101
|
+
if (c.reason) bits.push(c.reason);
|
|
102
|
+
if (c.conf !== undefined && c.conf !== null) bits.push("conf " + c.conf.toFixed(2));
|
|
103
|
+
if (c.tools !== null && c.tools !== undefined) bits.push(t.tools + " " + c.tools);
|
|
104
|
+
live.append(el("div", { class: "card" }, [
|
|
105
|
+
el("div", { class: "row1" }, [
|
|
106
|
+
el("span", { text: c.harness + " · " + c.agent }),
|
|
107
|
+
el("span", { text: c.tier + (c.group ? " → " + c.group : "") }),
|
|
108
|
+
]),
|
|
109
|
+
el("div", { class: "row2", text: c.session + " · " + bits.join(" · ") }),
|
|
110
|
+
]));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const body = $("mbridge-decisions");
|
|
114
|
+
body.textContent = "";
|
|
115
|
+
for (const d of v.decisions || []) {
|
|
116
|
+
const tr = document.createElement("tr");
|
|
117
|
+
const cells = [
|
|
118
|
+
[d.clock, "muted"],
|
|
119
|
+
[d.harness, ""],
|
|
120
|
+
[d.agent, ""],
|
|
121
|
+
[d.tier, ""],
|
|
122
|
+
[d.advised, d.differs ? "differs" : "muted"],
|
|
123
|
+
[d.reason, "muted"],
|
|
124
|
+
[d.conf === undefined || d.conf === null ? "—" : d.conf.toFixed(2), "num"],
|
|
125
|
+
];
|
|
126
|
+
for (const [text, cls] of cells) {
|
|
127
|
+
const td = document.createElement("td");
|
|
128
|
+
if (cls) td.className = cls;
|
|
129
|
+
td.textContent = text;
|
|
130
|
+
tr.append(td);
|
|
131
|
+
}
|
|
132
|
+
body.append(tr);
|
|
133
|
+
}
|
|
26
134
|
}
|
|
27
|
-
}
|
|
28
135
|
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
136
|
+
function paint() {
|
|
137
|
+
pick("periods", "period", state.period);
|
|
138
|
+
pick("sorts", "sort", state.sort);
|
|
139
|
+
// mbridge's part does not depend on magpie's usage
|
|
140
|
+
paintMbridge();
|
|
141
|
+
const s = state.summary;
|
|
142
|
+
if (!s) return;
|
|
143
|
+
$("since").textContent = V.since(s, lang);
|
|
34
144
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
145
|
+
$("tiles").replaceChildren(
|
|
146
|
+
...V.tiles(s, lang).map((tile) => {
|
|
147
|
+
const cell = document.createElement("div");
|
|
148
|
+
cell.className = "tile";
|
|
149
|
+
for (const [cls, value] of [["k", tile.label], ["v", tile.value], ["s", tile.sub]]) {
|
|
150
|
+
if (value === undefined) continue;
|
|
151
|
+
const e = document.createElement("div");
|
|
152
|
+
e.className = cls;
|
|
153
|
+
e.textContent = value;
|
|
154
|
+
cell.append(e);
|
|
155
|
+
}
|
|
156
|
+
return cell;
|
|
157
|
+
})
|
|
158
|
+
);
|
|
46
159
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
160
|
+
const rows = V.rows(s, state.sort, lang);
|
|
161
|
+
$("rows").replaceChildren(
|
|
162
|
+
...rows.map((r) => {
|
|
163
|
+
const tr = document.createElement("tr");
|
|
164
|
+
const name = document.createElement("td");
|
|
165
|
+
name.className = "model";
|
|
166
|
+
name.textContent = r.model;
|
|
167
|
+
const bar = document.createElement("span");
|
|
168
|
+
bar.className = "bar";
|
|
169
|
+
const fill = document.createElement("i");
|
|
170
|
+
fill.style.width = Math.max(2, Math.round(r.shareValue * 100)) + "%";
|
|
171
|
+
bar.append(fill);
|
|
172
|
+
name.append(bar);
|
|
173
|
+
const calls = document.createElement("td");
|
|
174
|
+
calls.className = "num";
|
|
175
|
+
calls.textContent = r.calls;
|
|
176
|
+
const tokens = document.createElement("td");
|
|
177
|
+
tokens.className = "num";
|
|
178
|
+
tokens.textContent = r.tokens;
|
|
179
|
+
const cost = document.createElement("td");
|
|
180
|
+
cost.className = "num";
|
|
181
|
+
cost.textContent = r.cost;
|
|
182
|
+
tr.append(name, calls, tokens, cost);
|
|
183
|
+
return tr;
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
$("empty").hidden = rows.length > 0;
|
|
187
|
+
$("empty").textContent = V.text(lang).empty;
|
|
188
|
+
$("error").hidden = !state.error;
|
|
189
|
+
if (state.error) {
|
|
190
|
+
$("error").textContent = V.text(lang).failed + ": " + state.error + " · " + V.text(lang).retry;
|
|
65
191
|
}
|
|
66
|
-
box.appendChild(cell);
|
|
67
192
|
}
|
|
68
193
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
for (const item of r.items) {
|
|
80
|
-
const tr = document.createElement("tr");
|
|
81
|
-
const name = document.createElement("td");
|
|
82
|
-
name.className = "model";
|
|
83
|
-
name.textContent = item.model;
|
|
84
|
-
const bar = document.createElement("span");
|
|
85
|
-
bar.className = "bar";
|
|
86
|
-
const fill = document.createElement("i");
|
|
87
|
-
fill.style.width = Math.round(item.shareValue * 100) + "%";
|
|
88
|
-
bar.appendChild(fill);
|
|
89
|
-
name.appendChild(bar);
|
|
90
|
-
tr.appendChild(name);
|
|
91
|
-
for (const key of ["calls", "tokens", "cost"]) {
|
|
92
|
-
const td = document.createElement("td");
|
|
93
|
-
td.className = "num";
|
|
94
|
-
td.textContent = item[key];
|
|
95
|
-
tr.appendChild(td);
|
|
194
|
+
// readOptions reads the plugin's options: the page's period and sort,
|
|
195
|
+
// and what mbridge published there.
|
|
196
|
+
async function readOptions() {
|
|
197
|
+
try {
|
|
198
|
+
const o = (await magpie.options.get()) || {};
|
|
199
|
+
if (V.PERIODS.includes(o.period)) state.period = o.period;
|
|
200
|
+
if (V.SORTS.includes(o.sort)) state.sort = o.sort;
|
|
201
|
+
state.mbridge = o.mbridge || null;
|
|
202
|
+
} catch {
|
|
203
|
+
// the options are not readable: keep what we have
|
|
96
204
|
}
|
|
97
|
-
body.appendChild(tr);
|
|
98
205
|
}
|
|
99
|
-
$("empty").textContent = r.items.length ? "" : t.emptyText;
|
|
100
|
-
|
|
101
|
-
$("disclaimer").textContent = t.disclaimer;
|
|
102
|
-
$("status-label").textContent = t.status + " ";
|
|
103
|
-
// the address is selectable text, not a link: the sandbox can navigate
|
|
104
|
-
// itself, but that is a one-way ticket (SP14 §2)
|
|
105
|
-
$("status-addr").textContent = t.statusWhere + " " + STATUS_URL;
|
|
106
|
-
$("restart-label").textContent = " · " + t.restart + " ";
|
|
107
|
-
$("error").textContent = state.error ? state.error + " · " + t.retry : "";
|
|
108
|
-
}
|
|
109
206
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
state.summary = await magpie.read("usage:summary", { period: state.period });
|
|
120
|
-
state.error = null;
|
|
121
|
-
} catch (err) {
|
|
122
|
-
state.error = (err && err.message) || String(err);
|
|
207
|
+
async function load() {
|
|
208
|
+
await readOptions();
|
|
209
|
+
try {
|
|
210
|
+
state.summary = await magpie.read("usage:summary", { period: state.period });
|
|
211
|
+
state.error = null;
|
|
212
|
+
} catch (e) {
|
|
213
|
+
state.error = (e && e.message) || String(e);
|
|
214
|
+
}
|
|
215
|
+
paint();
|
|
123
216
|
}
|
|
124
|
-
render();
|
|
125
|
-
}
|
|
126
217
|
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
218
|
+
// command asks mbridge to do something. The sequence number makes it
|
|
219
|
+
// one-shot: the page repeats it until mbridge answers, and mbridge
|
|
220
|
+
// runs it once.
|
|
221
|
+
async function command(id) {
|
|
222
|
+
state.cmdSeq += 1;
|
|
223
|
+
const cmd = { id: id, seq: state.cmdSeq };
|
|
224
|
+
try {
|
|
225
|
+
const o = (await magpie.options.get()) || {};
|
|
226
|
+
o.cmd = cmd;
|
|
227
|
+
await magpie.options.set(o);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
// mbridge will not hear it; the page says so
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
return true;
|
|
136
233
|
}
|
|
137
|
-
}
|
|
138
234
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
235
|
+
// magpie takes one options write a second: a quick second click is
|
|
236
|
+
// saved with the next one
|
|
237
|
+
let saving = null;
|
|
238
|
+
function save() {
|
|
239
|
+
if (saving) return;
|
|
240
|
+
saving = setTimeout(async () => {
|
|
241
|
+
saving = null;
|
|
242
|
+
try {
|
|
243
|
+
// the whole object is replaced: mbridge's key goes with it, and
|
|
244
|
+
// mbridge puts its snapshot back on its next round
|
|
245
|
+
opts = (await magpie.options.set({ ...opts, period: state.period, sort: state.sort })) || opts;
|
|
246
|
+
} catch {
|
|
247
|
+
// not remembering is not worth an error on the page
|
|
248
|
+
}
|
|
249
|
+
}, 1100);
|
|
147
250
|
}
|
|
148
|
-
}
|
|
149
251
|
|
|
150
|
-
(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
252
|
+
$("periods").addEventListener("click", (e) => {
|
|
253
|
+
const b = e.target.closest("button[data-period]");
|
|
254
|
+
if (!b || b.dataset.period === state.period) return;
|
|
255
|
+
state.period = b.dataset.period;
|
|
256
|
+
save();
|
|
257
|
+
load();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
$("sorts").addEventListener("click", (e) => {
|
|
261
|
+
const b = e.target.closest("button[data-sort]");
|
|
262
|
+
if (!b || b.dataset.sort === state.sort) return;
|
|
263
|
+
state.sort = b.dataset.sort;
|
|
264
|
+
save();
|
|
265
|
+
paint();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// the period and sort buttons carry their own words
|
|
269
|
+
for (const b of $("periods").querySelectorAll("button[data-period]")) {
|
|
270
|
+
b.textContent = V.periodLabel(b.dataset.period, lang);
|
|
271
|
+
}
|
|
272
|
+
for (const b of $("sorts").querySelectorAll("button[data-sort]")) {
|
|
273
|
+
b.textContent = V.sortLabel(b.dataset.sort, lang);
|
|
274
|
+
}
|
|
275
|
+
$("addr").textContent = V.STATUS_URL;
|
|
276
|
+
$("mbridge-ask").textContent = V.text(lang).mbridgeAsk;
|
|
277
|
+
$("mbridge-restart").textContent = V.text(lang).mbridgeRestart;
|
|
278
|
+
$("mbridge-ask").addEventListener("click", async () => {
|
|
279
|
+
const b = $("mbridge-ask");
|
|
280
|
+
const t = V.text(lang);
|
|
281
|
+
b.disabled = true;
|
|
282
|
+
b.textContent = t.mbridgeAsking;
|
|
283
|
+
const ok = await command("refresh");
|
|
284
|
+
if (!ok) { b.disabled = false; b.textContent = t.mbridgeAsk; }
|
|
285
|
+
// mbridge publishes and the next poll picks it up
|
|
286
|
+
setTimeout(() => { b.disabled = false; b.textContent = t.mbridgeAsk; load(); }, 3000);
|
|
287
|
+
});
|
|
288
|
+
$("mbridge-restart").addEventListener("click", async () => {
|
|
289
|
+
const b = $("mbridge-restart");
|
|
290
|
+
const t = V.text(lang);
|
|
291
|
+
b.disabled = true;
|
|
292
|
+
b.textContent = t.mbridgeRestarting;
|
|
293
|
+
const ok = await command("restart");
|
|
294
|
+
if (!ok) { b.disabled = false; b.textContent = t.mbridgeRestart; return; }
|
|
295
|
+
b.textContent = t.mbridgeRestartAsked;
|
|
157
296
|
});
|
|
158
|
-
|
|
297
|
+
words();
|
|
159
298
|
await load();
|
|
160
|
-
setInterval(load,
|
|
161
|
-
})();
|
|
299
|
+
setInterval(load, 60000);
|
|
300
|
+
})();
|
package/page/index.html
CHANGED
|
@@ -9,50 +9,82 @@
|
|
|
9
9
|
<script src="view.js"></script>
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
|
-
<main
|
|
12
|
+
<main class="root">
|
|
13
13
|
<header class="head">
|
|
14
|
-
<h1
|
|
15
|
-
<p
|
|
14
|
+
<h1 data-t="title">mbridge</h1>
|
|
15
|
+
<p class="subtitle" data-t="subtitle"></p>
|
|
16
|
+
<p class="since" id="since"></p>
|
|
16
17
|
</header>
|
|
17
18
|
|
|
18
19
|
<div class="bar">
|
|
19
|
-
<div
|
|
20
|
-
|
|
20
|
+
<div class="segmented" id="periods">
|
|
21
|
+
<button type="button" data-period="today" aria-selected="false"></button>
|
|
22
|
+
<button type="button" data-period="7d" aria-selected="false"></button>
|
|
23
|
+
<button type="button" data-period="30d" aria-selected="false"></button>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="segmented" id="sorts">
|
|
26
|
+
<button type="button" data-sort="tokens" aria-selected="false"></button>
|
|
27
|
+
<button type="button" data-sort="calls" aria-selected="false"></button>
|
|
28
|
+
<button type="button" data-sort="cost" aria-selected="false"></button>
|
|
29
|
+
</div>
|
|
21
30
|
</div>
|
|
22
31
|
|
|
23
|
-
<section
|
|
32
|
+
<section class="tiles" id="tiles"></section>
|
|
24
33
|
|
|
25
34
|
<section class="models">
|
|
26
|
-
<
|
|
27
|
-
<h2 id="models-label" class="label"></h2>
|
|
28
|
-
<span id="models-sort" class="muted"></span>
|
|
29
|
-
</div>
|
|
35
|
+
<h2 data-t="models">by model</h2>
|
|
30
36
|
<table class="table">
|
|
31
37
|
<thead>
|
|
32
38
|
<tr>
|
|
33
|
-
<th
|
|
34
|
-
<th class="num"
|
|
35
|
-
<th class="num"
|
|
36
|
-
<th class="num"
|
|
39
|
+
<th data-t="model">model</th>
|
|
40
|
+
<th class="num" data-t="calls">calls</th>
|
|
41
|
+
<th class="num" data-t="tokens">tokens</th>
|
|
42
|
+
<th class="num" data-t="cost">cost</th>
|
|
37
43
|
</tr>
|
|
38
44
|
</thead>
|
|
39
45
|
<tbody id="rows"></tbody>
|
|
40
46
|
</table>
|
|
41
|
-
<p
|
|
47
|
+
<p class="muted empty" id="empty" hidden></p>
|
|
48
|
+
</section>
|
|
49
|
+
|
|
50
|
+
<section class="mbridge">
|
|
51
|
+
<div class="mbridge-head">
|
|
52
|
+
<h2 data-t="mbridge">mbridge</h2>
|
|
53
|
+
<span id="mbridge-age" class="muted"></span>
|
|
54
|
+
</div>
|
|
55
|
+
<div id="mbridge-stats" class="stats"></div>
|
|
56
|
+
<h3 data-t="mbridgeLive">sessions running now</h3>
|
|
57
|
+
<div id="mbridge-live" class="cards"></div>
|
|
58
|
+
<h3 data-t="mbridgeDecisions">recent decisions</h3>
|
|
59
|
+
<table class="table mbridge-table">
|
|
60
|
+
<thead>
|
|
61
|
+
<tr>
|
|
62
|
+
<th></th><th data-t="harness">harness</th><th data-t="agent">agent</th>
|
|
63
|
+
<th data-t="tier">tier</th><th data-t="advised">advised</th>
|
|
64
|
+
<th data-t="reason">reason</th><th class="num" data-t="conf">confidence</th>
|
|
65
|
+
</tr>
|
|
66
|
+
</thead>
|
|
67
|
+
<tbody id="mbridge-decisions"></tbody>
|
|
68
|
+
</table>
|
|
69
|
+
<p id="mbridge-empty" class="muted"></p>
|
|
70
|
+
<div class="mbridge-actions">
|
|
71
|
+
<button type="button" id="mbridge-ask" class="btn"></button>
|
|
72
|
+
<button type="button" id="mbridge-restart" class="btn"></button>
|
|
73
|
+
</div>
|
|
42
74
|
</section>
|
|
43
75
|
|
|
44
76
|
<section class="notes">
|
|
45
|
-
<p
|
|
77
|
+
<p class="note" data-t="disclaimer"></p>
|
|
46
78
|
<p class="note">
|
|
47
|
-
<span id="status
|
|
48
|
-
<span
|
|
49
|
-
<span id="restart
|
|
50
|
-
<code class="cmd"
|
|
79
|
+
<span id="status"></span>
|
|
80
|
+
<span class="addr" id="addr" title="select and copy it"></span>
|
|
81
|
+
<span id="restart"></span>
|
|
82
|
+
<code class="cmd" id="cmd"></code>
|
|
51
83
|
</p>
|
|
52
84
|
</section>
|
|
53
85
|
|
|
54
|
-
<p
|
|
86
|
+
<p class="error" id="error" hidden></p>
|
|
55
87
|
</main>
|
|
56
88
|
<script src="app.js" defer></script>
|
|
57
89
|
</body>
|
|
58
|
-
</html>
|
|
90
|
+
</html>
|
package/page/style.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/* Only magpie's own variables, so the page follows the window's theme
|
|
2
|
-
|
|
1
|
+
/* Only magpie's own variables, so the page follows the window's theme in
|
|
2
|
+
both light and dark without knowing either. */
|
|
3
3
|
.root {
|
|
4
4
|
--pad: 12px;
|
|
5
5
|
padding: var(--pad);
|
|
@@ -7,30 +7,31 @@
|
|
|
7
7
|
color: var(--magpie-fg, #1a1a1a);
|
|
8
8
|
background: var(--magpie-bg, transparent);
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
.subtitle { margin: 2px 0
|
|
10
|
+
h1 { margin: 0; font-size: 1.05em; font-weight: 600; }
|
|
11
|
+
.subtitle { margin: 2px 0 0; color: var(--magpie-fg-muted, #6b6b6b); }
|
|
12
|
+
.since { margin: 0 0 8px; color: var(--magpie-fg-muted, #6b6b6b); font-size: .9em; }
|
|
12
13
|
.bar { display: flex; gap: 8px; justify-content: space-between; flex-wrap: wrap; margin-bottom: 10px; }
|
|
13
14
|
.segmented { display: flex; gap: 2px; }
|
|
14
|
-
.
|
|
15
|
-
font: inherit; color: inherit; cursor: pointer;
|
|
16
|
-
padding: 2px 8px; border-radius: 5px;
|
|
15
|
+
.segmented button {
|
|
16
|
+
font: inherit; color: inherit; cursor: pointer; padding: 2px 8px; border-radius: 5px;
|
|
17
17
|
border: 1px solid var(--magpie-border, #d8d8d8);
|
|
18
18
|
background: var(--magpie-bg-subtle, transparent);
|
|
19
19
|
}
|
|
20
|
-
.
|
|
20
|
+
.segmented button[aria-selected="true"] {
|
|
21
|
+
background: var(--magpie-accent, #2f6feb); border-color: transparent; color: #fff;
|
|
22
|
+
}
|
|
21
23
|
.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: 14px; }
|
|
22
24
|
.tile {
|
|
23
25
|
padding: 6px 8px; border-radius: 6px;
|
|
24
26
|
border: 1px solid var(--magpie-border, #e3e3e3);
|
|
25
27
|
background: var(--magpie-bg-subtle, transparent);
|
|
26
28
|
}
|
|
27
|
-
.tile
|
|
28
|
-
.tile
|
|
29
|
-
.tile
|
|
30
|
-
.
|
|
31
|
-
.models-head { display: flex; justify-content: space-between; align-items: baseline; }
|
|
29
|
+
.tile .k { color: var(--magpie-fg-muted, #6b6b6b); }
|
|
30
|
+
.tile .v { font-size: 1.15em; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
31
|
+
.tile .s { color: var(--magpie-fg-muted, #6b6b6b); }
|
|
32
|
+
.models h2 { margin: 0 0 4px; font-size: 1em; font-weight: 600; }
|
|
32
33
|
.muted { color: var(--magpie-fg-muted, #6b6b6b); }
|
|
33
|
-
.table { width: 100%; border-collapse: collapse;
|
|
34
|
+
.table { width: 100%; border-collapse: collapse; }
|
|
34
35
|
.table th, .table td { text-align: left; padding: 3px 4px; border-bottom: 1px solid var(--magpie-border, #e3e3e3); }
|
|
35
36
|
.table .num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
36
37
|
.table th.num { text-align: right; color: var(--magpie-fg-muted, #6b6b6b); font-weight: 500; }
|
|
@@ -40,7 +41,27 @@
|
|
|
40
41
|
.empty { margin: 6px 0; }
|
|
41
42
|
.notes { margin-top: 14px; }
|
|
42
43
|
.note { margin: 4px 0; color: var(--magpie-fg-muted, #6b6b6b); }
|
|
43
|
-
/* the address is selectable text, not a link
|
|
44
|
+
/* the address is selectable text, not a link: the sandbox may navigate
|
|
45
|
+
itself, but that is a one-way ticket (SP14 §2) */
|
|
44
46
|
.addr { user-select: all; color: var(--magpie-fg, inherit); }
|
|
45
47
|
.cmd { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--magpie-fg, inherit); }
|
|
46
|
-
.error { color: var(--magpie-danger, #b42318);
|
|
48
|
+
.error { color: var(--magpie-danger, #b42318); margin: 8px 0 0; }
|
|
49
|
+
|
|
50
|
+
/* the mbridge section: what mbridge published into the options */
|
|
51
|
+
.mbridge { margin-top: 16px; padding-top: 10px; border-top: 1px solid var(--magpie-border, #e3e3e3); }
|
|
52
|
+
.mbridge-head { display: flex; justify-content: space-between; align-items: baseline; }
|
|
53
|
+
.mbridge-head h2 { margin: 0 0 4px; font-size: 1em; font-weight: 600; }
|
|
54
|
+
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin: 6px 0 10px; }
|
|
55
|
+
.stat { padding: 5px 7px; border: 1px solid var(--magpie-border, #e3e3e3); border-radius: 6px; }
|
|
56
|
+
.stat b { display: block; font-size: 1.1em; font-variant-numeric: tabular-nums; }
|
|
57
|
+
.stat span { color: var(--magpie-fg-muted, #6b6b6b); font-size: .85em; }
|
|
58
|
+
.cards { display: grid; gap: 5px; margin-bottom: 10px; }
|
|
59
|
+
.card { padding: 5px 7px; border: 1px solid var(--magpie-border, #e3e3e3); border-radius: 6px; }
|
|
60
|
+
.card .row1 { display: flex; justify-content: space-between; gap: .5rem; }
|
|
61
|
+
.card .row2 { color: var(--magpie-fg-muted, #6b6b6b); font-size: .85em; }
|
|
62
|
+
.mbridge-table td { padding: 2px 4px; }
|
|
63
|
+
.mbridge-table td.differs { font-weight: 600; }
|
|
64
|
+
.mbridge-actions { display: flex; gap: 6px; margin-top: 8px; }
|
|
65
|
+
.btn { font: inherit; padding: 2px 9px; border-radius: 5px; cursor: pointer;
|
|
66
|
+
border: 1px solid var(--magpie-border, #d8d8d8); background: var(--magpie-bg-subtle, transparent); color: inherit; }
|
|
67
|
+
.btn[disabled] { opacity: .6; cursor: default; }
|
package/page/view.js
CHANGED
|
@@ -1,178 +1,293 @@
|
|
|
1
|
-
// view.js is
|
|
2
|
-
//
|
|
1
|
+
// view.js is what the page shows, worked out from usage:summary without
|
|
2
|
+
// the DOM, so a test can check it.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// It is a classic script, not a module: a magpie page runs in a sandbox
|
|
5
|
+
// whose CSP serves the plugin's own files, and the community's usage
|
|
6
|
+
// page does it this way too (globalThis.UsageView). A module script
|
|
7
|
+
// would be a different thing to load, and the test below loads this file
|
|
8
|
+
// the same way the page does.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
10
|
+
(() => {
|
|
11
|
+
const TEXT = {
|
|
12
|
+
en: {
|
|
13
|
+
title: "mbridge",
|
|
14
|
+
subtitle: "what magpie's ledger recorded",
|
|
15
|
+
calls: "calls",
|
|
16
|
+
errors: "errors",
|
|
17
|
+
errorShare: "of the calls",
|
|
18
|
+
tokens: "tokens",
|
|
19
|
+
cost: "cost",
|
|
20
|
+
models: "by model",
|
|
21
|
+
model: "model",
|
|
22
|
+
today: "today",
|
|
23
|
+
"7d": "7 days",
|
|
24
|
+
"30d": "30 days",
|
|
25
|
+
sortTokens: "tokens",
|
|
26
|
+
sortCalls: "calls",
|
|
27
|
+
sortCost: "cost",
|
|
28
|
+
empty: "no calls in this period",
|
|
29
|
+
failed: "couldn't read usage",
|
|
30
|
+
retry: "try again in a minute",
|
|
31
|
+
since: "since",
|
|
32
|
+
disclaimer:
|
|
33
|
+
"This is everything magpie recorded. It is mbridge's usage only when every client goes through mbridge; whatever you use magpie for directly is in here too.",
|
|
34
|
+
status: "mbridge's own status:",
|
|
35
|
+
restart: "restart it with",
|
|
36
|
+
mbridge: "mbridge",
|
|
37
|
+
mbridgeNone: "mbridge has not published anything yet",
|
|
38
|
+
mbridgeAge: "published",
|
|
39
|
+
mbridgeLive: "sessions running now",
|
|
40
|
+
mbridgeNoLive: "no session is going through mbridge right now",
|
|
41
|
+
mbridgeDecisions: "recent decisions",
|
|
42
|
+
mbridgeWindow: "the last",
|
|
43
|
+
mbridgeTurns: "turns",
|
|
44
|
+
mbridgeCoverage: "asked the classifier",
|
|
45
|
+
mbridgeFailures: "failures",
|
|
46
|
+
mbridgeAsk: "ask for fresh numbers",
|
|
47
|
+
mbridgeAsking: "asking…",
|
|
48
|
+
mbridgeRestart: "restart mbridge",
|
|
49
|
+
mbridgeRestarting: "restarting…",
|
|
50
|
+
mbridgeRestartAsked: "restart asked for",
|
|
51
|
+
mbridgeRestartRefused: "refused",
|
|
52
|
+
tier: "tier",
|
|
53
|
+
advised: "advised",
|
|
54
|
+
reason: "reason",
|
|
55
|
+
conf: "confidence",
|
|
56
|
+
unknown: "unknown",
|
|
57
|
+
running: "running",
|
|
58
|
+
notRunning: "installed, not running",
|
|
59
|
+
notInstalled: "not installed",
|
|
60
|
+
tools: "tools",
|
|
61
|
+
noDecisions: "no decision yet",
|
|
62
|
+
},
|
|
63
|
+
zh: {
|
|
64
|
+
title: "mbridge",
|
|
65
|
+
subtitle: "magpie 记下的全部用量",
|
|
66
|
+
calls: "调用",
|
|
67
|
+
errors: "错误",
|
|
68
|
+
errorShare: "占调用",
|
|
69
|
+
tokens: "Token",
|
|
70
|
+
cost: "费用",
|
|
71
|
+
models: "按模型",
|
|
72
|
+
model: "模型",
|
|
73
|
+
today: "今天",
|
|
74
|
+
"7d": "7 天",
|
|
75
|
+
"30d": "30 天",
|
|
76
|
+
sortTokens: "Token",
|
|
77
|
+
sortCalls: "调用",
|
|
78
|
+
sortCost: "费用",
|
|
79
|
+
empty: "这个区间没有调用",
|
|
80
|
+
failed: "读不到 magpie 的用量",
|
|
81
|
+
retry: "一分钟后自动重试",
|
|
82
|
+
since: "起",
|
|
83
|
+
disclaimer:
|
|
84
|
+
"这是 magpie 记下的全部用量。客户端都走 mbridge 时它才等于 mbridge 的用量;你直接用 magpie 的那部分也在里面。",
|
|
85
|
+
status: "mbridge 自己的状态:",
|
|
86
|
+
restart: "重启用",
|
|
87
|
+
mbridge: "mbridge",
|
|
88
|
+
mbridgeNone: "mbridge 还没有发布过任何东西",
|
|
89
|
+
mbridgeAge: "发布于",
|
|
90
|
+
mbridgeLive: "正在选档的会话",
|
|
91
|
+
mbridgeNoLive: "现在没有会话在走 mbridge",
|
|
92
|
+
mbridgeDecisions: "最近判档",
|
|
93
|
+
mbridgeWindow: "最近",
|
|
94
|
+
mbridgeTurns: "轮",
|
|
95
|
+
mbridgeCoverage: "问了分类器",
|
|
96
|
+
mbridgeFailures: "失败",
|
|
97
|
+
mbridgeAsk: "要最新数字",
|
|
98
|
+
mbridgeAsking: "正在要…",
|
|
99
|
+
mbridgeRestart: "重启 mbridge",
|
|
100
|
+
mbridgeRestarting: "正在重启…",
|
|
101
|
+
mbridgeRestartAsked: "已发出重启指令",
|
|
102
|
+
mbridgeRestartRefused: "被拒绝",
|
|
103
|
+
tier: "档",
|
|
104
|
+
advised: "建议",
|
|
105
|
+
reason: "reason",
|
|
106
|
+
conf: "置信度",
|
|
107
|
+
unknown: "未知",
|
|
108
|
+
running: "在跑",
|
|
109
|
+
notRunning: "装了,没在跑",
|
|
110
|
+
notInstalled: "没装",
|
|
111
|
+
tools: "工具",
|
|
112
|
+
noDecisions: "还没有判档",
|
|
113
|
+
},
|
|
114
|
+
};
|
|
57
115
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
if (Math.abs(v) >= 1e9) return (v / 1e9).toFixed(2) + "B";
|
|
62
|
-
if (Math.abs(v) >= 1e6) return (v / 1e6).toFixed(2) + "M";
|
|
63
|
-
if (Math.abs(v) >= 1e3) return v.toLocaleString("en-US");
|
|
64
|
-
return String(v);
|
|
65
|
-
}
|
|
116
|
+
const PERIODS = ["today", "7d", "30d"];
|
|
117
|
+
const SORTS = ["tokens", "calls", "cost"];
|
|
118
|
+
const STATUS_URL = "http://127.0.0.1:3426/status";
|
|
66
119
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (v === 0) return "$0";
|
|
72
|
-
if (v > 0 && v < 0.01) return "<$0.01";
|
|
73
|
-
if (v < 10) return "$" + v.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
|
|
74
|
-
if (v < 1000) return "$" + v.toFixed(2);
|
|
75
|
-
return "$" + v.toFixed(0);
|
|
76
|
-
}
|
|
120
|
+
// text is the words for a language: zh for anything that starts with zh.
|
|
121
|
+
function text(lang) {
|
|
122
|
+
return String(lang || "").toLowerCase().startsWith("zh") ? TEXT.zh : TEXT.en;
|
|
123
|
+
}
|
|
77
124
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
125
|
+
// count is a number a person reads: 1,234 or 1.23M.
|
|
126
|
+
function count(n) {
|
|
127
|
+
n = Number(n) || 0;
|
|
128
|
+
const a = Math.abs(n);
|
|
129
|
+
if (a >= 1e9) return (n / 1e9).toFixed(2) + "B";
|
|
130
|
+
if (a >= 1e6) return (n / 1e6).toFixed(2) + "M";
|
|
131
|
+
if (a >= 1e3) return n.toLocaleString("en-US");
|
|
132
|
+
return String(n);
|
|
133
|
+
}
|
|
87
134
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
135
|
+
// money is a price, never NaN, and never "$0.00" for something real.
|
|
136
|
+
function money(n) {
|
|
137
|
+
n = Number(n);
|
|
138
|
+
if (!isFinite(n)) return "$0";
|
|
139
|
+
if (n === 0) return "$0";
|
|
140
|
+
if (n > 0 && n < 0.01) return "<$0.01";
|
|
141
|
+
if (n < 10) return "$" + n.toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
|
|
142
|
+
if (n < 1000) return "$" + n.toFixed(2);
|
|
143
|
+
return "$" + n.toFixed(0);
|
|
144
|
+
}
|
|
91
145
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
146
|
+
// share is a percentage, and never NaN.
|
|
147
|
+
function pct(part, whole) {
|
|
148
|
+
const p = Number(part) || 0;
|
|
149
|
+
const w = Number(whole) || 0;
|
|
150
|
+
if (w <= 0) return "0%";
|
|
151
|
+
const v = (100 * p) / w;
|
|
152
|
+
if (!isFinite(v)) return "0%";
|
|
153
|
+
return (Math.round(v * 10) / 10).toString() + "%";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// tiles are the four numbers at the top: calls, errors, tokens, cost.
|
|
157
|
+
function tiles(summary, lang) {
|
|
158
|
+
const t = text(lang);
|
|
159
|
+
const s = summary || {};
|
|
160
|
+
return [
|
|
161
|
+
{ key: "calls", label: t.calls, value: count(s.calls) },
|
|
162
|
+
{
|
|
163
|
+
key: "errors",
|
|
164
|
+
label: t.errors,
|
|
165
|
+
value: count(s.errors),
|
|
166
|
+
sub: t.errorShare + " " + pct(s.errors, s.calls),
|
|
167
|
+
},
|
|
168
|
+
{ key: "tokens", label: t.tokens, value: count((s.input || 0) + (s.output || 0)) },
|
|
169
|
+
{ key: "cost", label: t.cost, value: money(s.cost) },
|
|
170
|
+
];
|
|
171
|
+
}
|
|
108
172
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const total = sorted.reduce((a, m) => a + value(m), 0);
|
|
120
|
-
const labels = t.sort;
|
|
121
|
-
return {
|
|
122
|
-
sort: key,
|
|
123
|
-
label: t.models,
|
|
124
|
-
sortLabel: labels[key] || key,
|
|
125
|
-
total,
|
|
126
|
-
items: sorted.map((m) => ({
|
|
173
|
+
// rows are the model table, sorted by tokens, calls or cost, with each
|
|
174
|
+
// model's share of the total.
|
|
175
|
+
function rows(summary, sort, lang) {
|
|
176
|
+
const s = summary || {};
|
|
177
|
+
const models = Array.isArray(s.models) ? s.models : [];
|
|
178
|
+
const key = sort === "calls" ? "calls" : sort === "cost" ? "cost" : "tokens";
|
|
179
|
+
const value = (m) => (key === "tokens" ? (m.input || 0) + (m.output || 0) : m[key] || 0);
|
|
180
|
+
const sorted = models.slice().sort((a, b) => value(b) - value(a));
|
|
181
|
+
const total = sorted.reduce((a, m) => a + value(m), 0);
|
|
182
|
+
return sorted.map((m) => ({
|
|
127
183
|
model: m.model || "?",
|
|
128
184
|
calls: count(m.calls),
|
|
129
185
|
tokens: count((m.input || 0) + (m.output || 0)),
|
|
130
186
|
cost: money(m.cost),
|
|
131
187
|
share: pct(value(m), total),
|
|
132
188
|
shareValue: total > 0 ? value(m) / total : 0,
|
|
133
|
-
}))
|
|
134
|
-
}
|
|
135
|
-
}
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
136
191
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
192
|
+
// since is when the period started.
|
|
193
|
+
function since(summary, lang) {
|
|
194
|
+
const t = text(lang);
|
|
195
|
+
const s = summary || {};
|
|
196
|
+
return s.since ? t.since + " " + s.since : "";
|
|
197
|
+
}
|
|
142
198
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
199
|
+
// periodLabel is the name of a period.
|
|
200
|
+
function periodLabel(period, lang) {
|
|
201
|
+
return text(lang)[period] || period;
|
|
202
|
+
}
|
|
148
203
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
title: t.title,
|
|
155
|
-
subtitle: t.subtitle,
|
|
156
|
-
since: s.since ? t.since + " " + s.since : "",
|
|
157
|
-
empty: !s.calls,
|
|
158
|
-
emptyText: t.empty,
|
|
159
|
-
error: t.error,
|
|
160
|
-
retry: t.retry,
|
|
161
|
-
disclaimer: t.disclaimer,
|
|
162
|
-
status: t.status,
|
|
163
|
-
statusWhere: t.statusWhere,
|
|
164
|
-
restart: t.restart,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
204
|
+
// sortLabel is the name of a sort.
|
|
205
|
+
function sortLabel(sort, lang) {
|
|
206
|
+
const t = text(lang);
|
|
207
|
+
return t["sort" + sort.charAt(0).toUpperCase() + sort.slice(1)] || sort;
|
|
208
|
+
}
|
|
167
209
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
};
|
|
210
|
+
// mbridgeView is what the page shows of mbridge, from the snapshot
|
|
211
|
+
// mbridge published into the plugin's options (SP14 §7).
|
|
212
|
+
function mbridgeView(snapshot, lang) {
|
|
213
|
+
const t = text(lang);
|
|
214
|
+
if (!snapshot) {
|
|
215
|
+
return { none: true, text: t.mbridgeNone };
|
|
216
|
+
}
|
|
217
|
+
const s = snapshot;
|
|
218
|
+
const cov = s.window && s.window.coverage ? s.window.coverage : { asked: 0, total: 0 };
|
|
219
|
+
const tiers = (s.window && s.window.tiers) || {};
|
|
220
|
+
const reasons = (s.window && s.window.reasons) || {};
|
|
221
|
+
const turnCount = Object.keys(tiers).reduce((a, k) => a + tiers[k], 0);
|
|
222
|
+
const topReason = Object.keys(reasons).sort((a, b) => reasons[b] - reasons[a])[0] || "";
|
|
223
|
+
const live = s.live || [];
|
|
224
|
+
// the decisions this process made are in memory (a restart empties
|
|
225
|
+
// them): the window's turns are the fallback
|
|
226
|
+
const decisions = (s.decisions && s.decisions.length)
|
|
227
|
+
? s.decisions
|
|
228
|
+
: ((s.window && s.window.turns) || []);
|
|
229
|
+
const at = s.at ? new Date(s.at) : null;
|
|
230
|
+
const age = at && !isNaN(at)
|
|
231
|
+
? Math.max(0, Math.round((Date.now() - at.getTime()) / 1000))
|
|
232
|
+
: null;
|
|
233
|
+
return {
|
|
234
|
+
none: false,
|
|
235
|
+
version: s.version || "",
|
|
236
|
+
pid: s.pid || 0,
|
|
237
|
+
uptime: s.uptime_s || 0,
|
|
238
|
+
listen: s.listen || "",
|
|
239
|
+
service: (s.service && s.service.running) ? t.running : ((s.service && s.service.installed) ? t.notRunning : t.notInstalled),
|
|
240
|
+
magpie: s.magpie_ok ? (s.magpie_version || "") : "—",
|
|
241
|
+
turnCount: turnCount,
|
|
242
|
+
coverage: cov.total ? Math.round((100 * cov.asked) / cov.total) + "%" : "—",
|
|
243
|
+
failures: (s.window && s.window.classify && s.window.classify.failures) || 0,
|
|
244
|
+
topReason: topReason,
|
|
245
|
+
age: age,
|
|
246
|
+
ageText: age === null ? "" : (age < 60 ? age + "s" : (age < 3600 ? Math.round(age / 60) + "m" : Math.round(age / 3600) + "h")),
|
|
247
|
+
live: live.map((c) => ({
|
|
248
|
+
session: (c.session || "").slice(0, 8),
|
|
249
|
+
harness: c.harness || t.unknown,
|
|
250
|
+
agent: c.agent || "",
|
|
251
|
+
tier: c.tier || "",
|
|
252
|
+
group: c.group || "",
|
|
253
|
+
reason: c.reason || "",
|
|
254
|
+
conf: c.tier_confidence,
|
|
255
|
+
turns: c.turns || 0,
|
|
256
|
+
tools: c.agent === "gateway" ? (c.tools || 0) + "/" + (c.tool_failures || 0) : null,
|
|
257
|
+
last: c.last_at || "",
|
|
258
|
+
})),
|
|
259
|
+
decisions: decisions.slice(0, 12).map((ev) => ({
|
|
260
|
+
time: ev.t || "",
|
|
261
|
+
clock: ev.t ? ev.t.slice(11, 19) : "",
|
|
262
|
+
harness: ev.harness || t.unknown,
|
|
263
|
+
agent: ev.agent || "",
|
|
264
|
+
tier: ev.tier || "",
|
|
265
|
+
advised: ev.classified_tier || "",
|
|
266
|
+
differs: !!(ev.classified_tier && ev.classified_tier !== ev.tier),
|
|
267
|
+
reason: ev.reason || "",
|
|
268
|
+
source: ev.classifier || ev.source || "",
|
|
269
|
+
conf: ev.tier_confidence,
|
|
270
|
+
ms: ev.latency_ms || 0,
|
|
271
|
+
})),
|
|
272
|
+
emptyLive: live.length === 0,
|
|
273
|
+
noDecisions: decisions.length === 0,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
globalThis.MbridgeView = {
|
|
278
|
+
TEXT,
|
|
279
|
+
PERIODS,
|
|
280
|
+
SORTS,
|
|
281
|
+
STATUS_URL,
|
|
282
|
+
text,
|
|
283
|
+
count,
|
|
284
|
+
money,
|
|
285
|
+
pct,
|
|
286
|
+
tiles,
|
|
287
|
+
rows,
|
|
288
|
+
since,
|
|
289
|
+
periodLabel,
|
|
290
|
+
sortLabel,
|
|
291
|
+
mbridgeView,
|
|
292
|
+
};
|
|
293
|
+
})();
|