@weiping/mbridge-page 0.2.1 → 0.4.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 +7 -9
- package/page/app.js +83 -286
- package/page/index.html +2 -77
- package/page/style.css +86 -66
- package/page/view.js +553 -262
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weiping/mbridge-page",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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": [
|
|
@@ -23,19 +23,17 @@
|
|
|
23
23
|
],
|
|
24
24
|
"magpie": {
|
|
25
25
|
"page": {
|
|
26
|
-
"title": "
|
|
26
|
+
"title": "鹊桥",
|
|
27
27
|
"titles": {
|
|
28
|
-
"zh": "
|
|
29
|
-
"zh-TW": "
|
|
30
|
-
"ja": "
|
|
31
|
-
"de": "
|
|
28
|
+
"zh": "鹊桥",
|
|
29
|
+
"zh-TW": "鵲橋",
|
|
30
|
+
"ja": "鵲橋",
|
|
31
|
+
"de": "Magpie Bridge"
|
|
32
32
|
},
|
|
33
33
|
"file": "page/index.html",
|
|
34
34
|
"icon": "page/icon.svg"
|
|
35
35
|
},
|
|
36
|
-
"permissions": [
|
|
37
|
-
"usage:summary"
|
|
38
|
-
],
|
|
36
|
+
"permissions": [],
|
|
39
37
|
"options": {
|
|
40
38
|
"period": "7d",
|
|
41
39
|
"sort": "tokens"
|
package/page/app.js
CHANGED
|
@@ -1,314 +1,111 @@
|
|
|
1
|
-
// app.js
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
// A classic script, like the community's usage page: it reads the view
|
|
7
|
-
// from globalThis.
|
|
1
|
+
// app.js powers the 鹊桥 plugin page inside magpie's window.
|
|
2
|
+
// It reads the snapshot mbridge published into magpie.options,
|
|
3
|
+
// renders the full dashboard, and dispatches user actions (window tabs,
|
|
4
|
+
// restart, and refresh).
|
|
5
|
+
|
|
8
6
|
(async () => {
|
|
9
7
|
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
8
|
const theme = await magpie.ready;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
};
|
|
47
|
-
|
|
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
|
-
}
|
|
57
|
-
|
|
58
|
-
function pick(group, attr, value) {
|
|
59
|
-
$(group).querySelectorAll("button").forEach((b) => {
|
|
60
|
-
b.setAttribute("aria-selected", String(b.dataset[attr] === value));
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
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);
|
|
9
|
+
if (theme && theme.lang) {
|
|
10
|
+
if (theme.lang.toLowerCase().startsWith("zh")) {
|
|
11
|
+
// view.js defines lang globally inside its IIFE, but we can also set navigator/locale
|
|
133
12
|
}
|
|
134
13
|
}
|
|
135
14
|
|
|
136
|
-
|
|
137
|
-
|
|
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);
|
|
144
|
-
|
|
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
|
-
);
|
|
15
|
+
let opts = {};
|
|
16
|
+
const current = { window: "7d" };
|
|
159
17
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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;
|
|
191
|
-
}
|
|
18
|
+
function fail(message) {
|
|
19
|
+
const root = document.getElementById("root");
|
|
20
|
+
root.textContent = "";
|
|
21
|
+
const h1 = document.createElement("h1");
|
|
22
|
+
h1.textContent = "鹊桥";
|
|
23
|
+
const banner = document.createElement("div");
|
|
24
|
+
banner.className = "banner bad err";
|
|
25
|
+
banner.textContent = message;
|
|
26
|
+
root.append(h1, banner);
|
|
192
27
|
}
|
|
193
28
|
|
|
194
|
-
|
|
195
|
-
// and what mbridge published there.
|
|
196
|
-
async function readOptions() {
|
|
29
|
+
async function command(id) {
|
|
197
30
|
try {
|
|
198
31
|
const o = (await magpie.options.get()) || {};
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (V.PERIODS.includes(o.period)) state.period = o.period;
|
|
203
|
-
if (V.SORTS.includes(o.sort)) state.sort = o.sort;
|
|
204
|
-
state.mbridge = o.mbridge || null;
|
|
32
|
+
o.cmd = { id: id, seq: Date.now() };
|
|
33
|
+
await magpie.options.set(o);
|
|
34
|
+
return true;
|
|
205
35
|
} catch {
|
|
206
|
-
|
|
36
|
+
return false;
|
|
207
37
|
}
|
|
208
38
|
}
|
|
209
39
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
state.summary = await magpie.read("usage:summary", { period: state.period });
|
|
214
|
-
state.error = null;
|
|
215
|
-
} catch (e) {
|
|
216
|
-
state.error = (e && e.message) || String(e);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async function load() {
|
|
221
|
-
await readOptions();
|
|
222
|
-
await loadUsage();
|
|
223
|
-
paint();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// command asks mbridge to do something. The sequence number makes it
|
|
227
|
-
// one-shot: the page repeats it until mbridge answers, and mbridge
|
|
228
|
-
// runs it once.
|
|
229
|
-
async function command(id) {
|
|
230
|
-
state.cmdSeq += 1;
|
|
231
|
-
const cmd = { id: id, seq: state.cmdSeq };
|
|
40
|
+
async function setWindow(kind) {
|
|
41
|
+
current.window = kind;
|
|
232
42
|
try {
|
|
233
43
|
const o = (await magpie.options.get()) || {};
|
|
234
|
-
o.
|
|
44
|
+
o.period = kind;
|
|
235
45
|
await magpie.options.set(o);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
return true;
|
|
46
|
+
// Ask mbridge to immediately publish the updated window
|
|
47
|
+
await command("refresh");
|
|
48
|
+
} catch {}
|
|
241
49
|
}
|
|
242
50
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
saving = setTimeout(async () => {
|
|
249
|
-
saving = null;
|
|
250
|
-
try {
|
|
251
|
-
// the whole object is replaced: mbridge's key goes with it, and
|
|
252
|
-
// mbridge puts its snapshot back on its next round
|
|
253
|
-
opts = (await magpie.options.set({ ...opts, period: state.period, sort: state.sort })) || opts;
|
|
254
|
-
} catch {
|
|
255
|
-
// not remembering is not worth an error on the page
|
|
51
|
+
async function readOptions() {
|
|
52
|
+
try {
|
|
53
|
+
opts = (await magpie.options.get()) || {};
|
|
54
|
+
if (opts.period && (opts.period === "1d" || opts.period === "7d" || opts.period === "30d")) {
|
|
55
|
+
current.window = opts.period;
|
|
256
56
|
}
|
|
257
|
-
|
|
57
|
+
return opts.mbridge || null;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
258
61
|
}
|
|
259
62
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
});
|
|
63
|
+
function renderView(data) {
|
|
64
|
+
if (!data) {
|
|
65
|
+
fail("鹊桥 正在等待 mbridge 发布快照数据…");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
V.render(data);
|
|
267
69
|
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
70
|
+
// Wire up tab switching in the rendered view
|
|
71
|
+
const tabButtons = document.querySelectorAll(".tabs button");
|
|
72
|
+
tabButtons.forEach((btn) => {
|
|
73
|
+
const kind = btn.textContent.trim();
|
|
74
|
+
if (kind === "1d" || kind === "7d" || kind === "30d") {
|
|
75
|
+
btn.onclick = async () => {
|
|
76
|
+
await setWindow(kind);
|
|
77
|
+
// optimistically update tab highlight
|
|
78
|
+
tabButtons.forEach((b) => b.setAttribute("aria-pressed", String(b.textContent.trim() === kind)));
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
275
82
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
83
|
+
// Wire up the restart button in the rendered view
|
|
84
|
+
const restartBtn = document.querySelector(".restart");
|
|
85
|
+
if (restartBtn) {
|
|
86
|
+
restartBtn.onclick = async () => {
|
|
87
|
+
restartBtn.disabled = true;
|
|
88
|
+
restartBtn.textContent = "正在重启…";
|
|
89
|
+
const ok = await command("restart");
|
|
90
|
+
if (ok) {
|
|
91
|
+
restartBtn.textContent = "已发出重启指令";
|
|
92
|
+
} else {
|
|
93
|
+
restartBtn.textContent = "指令发送失败";
|
|
94
|
+
restartBtn.disabled = false;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
279
98
|
}
|
|
280
|
-
|
|
281
|
-
|
|
99
|
+
|
|
100
|
+
async function poll() {
|
|
101
|
+
const snap = await readOptions();
|
|
102
|
+
if (snap) {
|
|
103
|
+
renderView(snap);
|
|
104
|
+
} else {
|
|
105
|
+
fail("鹊桥 正在等待 mbridge 发布快照数据…");
|
|
106
|
+
}
|
|
282
107
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
$("mbridge-ask").addEventListener("click", async () => {
|
|
287
|
-
const b = $("mbridge-ask");
|
|
288
|
-
const t = V.text(lang);
|
|
289
|
-
b.disabled = true;
|
|
290
|
-
b.textContent = t.mbridgeAsking;
|
|
291
|
-
const ok = await command("refresh");
|
|
292
|
-
if (!ok) { b.disabled = false; b.textContent = t.mbridgeAsk; }
|
|
293
|
-
// mbridge publishes and the next poll picks it up
|
|
294
|
-
setTimeout(() => { b.disabled = false; b.textContent = t.mbridgeAsk; load(); }, 3000);
|
|
295
|
-
});
|
|
296
|
-
$("mbridge-restart").addEventListener("click", async () => {
|
|
297
|
-
const b = $("mbridge-restart");
|
|
298
|
-
const t = V.text(lang);
|
|
299
|
-
b.disabled = true;
|
|
300
|
-
b.textContent = t.mbridgeRestarting;
|
|
301
|
-
const ok = await command("restart");
|
|
302
|
-
if (!ok) { b.disabled = false; b.textContent = t.mbridgeRestart; return; }
|
|
303
|
-
b.textContent = t.mbridgeRestartAsked;
|
|
304
|
-
});
|
|
305
|
-
words();
|
|
306
|
-
await load();
|
|
307
|
-
// magpie's ledger moves slowly; mbridge's snapshot moves every fifteen
|
|
308
|
-
// seconds, and reading the options is free
|
|
309
|
-
setInterval(loadUsage, 60000);
|
|
310
|
-
setInterval(async () => {
|
|
311
|
-
await readOptions();
|
|
312
|
-
paintMbridge();
|
|
313
|
-
}, 5000);
|
|
108
|
+
|
|
109
|
+
await poll();
|
|
110
|
+
setInterval(poll, 5000);
|
|
314
111
|
})();
|
package/page/index.html
CHANGED
|
@@ -3,88 +3,13 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
-
<title
|
|
6
|
+
<title>鹊桥</title>
|
|
7
7
|
<link rel="stylesheet" href="style.css">
|
|
8
8
|
<script src="_magpie/bridge.js"></script>
|
|
9
9
|
<script src="view.js"></script>
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
|
-
<main
|
|
13
|
-
<header class="head">
|
|
14
|
-
<h1 data-t="title">mbridge</h1>
|
|
15
|
-
<p class="subtitle" data-t="subtitle"></p>
|
|
16
|
-
<p class="since" id="since"></p>
|
|
17
|
-
</header>
|
|
18
|
-
|
|
19
|
-
<div class="bar">
|
|
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>
|
|
30
|
-
</div>
|
|
31
|
-
|
|
32
|
-
<section class="tiles" id="tiles"></section>
|
|
33
|
-
|
|
34
|
-
<section class="models">
|
|
35
|
-
<h2 data-t="models">by model</h2>
|
|
36
|
-
<table class="table">
|
|
37
|
-
<thead>
|
|
38
|
-
<tr>
|
|
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>
|
|
43
|
-
</tr>
|
|
44
|
-
</thead>
|
|
45
|
-
<tbody id="rows"></tbody>
|
|
46
|
-
</table>
|
|
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>
|
|
74
|
-
</section>
|
|
75
|
-
|
|
76
|
-
<section class="notes">
|
|
77
|
-
<p class="note" data-t="disclaimer"></p>
|
|
78
|
-
<p class="note">
|
|
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>
|
|
83
|
-
</p>
|
|
84
|
-
</section>
|
|
85
|
-
|
|
86
|
-
<p class="error" id="error" hidden></p>
|
|
87
|
-
</main>
|
|
12
|
+
<main id="root"></main>
|
|
88
13
|
<script src="app.js" defer></script>
|
|
89
14
|
</body>
|
|
90
15
|
</html>
|