@weiping/mbridge-page 0.1.1 → 0.2.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/package.json +1 -1
- package/page/app.js +163 -2
- package/page/index.html +26 -0
- package/page/style.css +19 -0
- package/page/view.js +120 -0
package/package.json
CHANGED
package/page/app.js
CHANGED
|
@@ -8,6 +8,21 @@
|
|
|
8
8
|
(async () => {
|
|
9
9
|
const V = globalThis.MbridgeView;
|
|
10
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
|
+
}
|
|
11
26
|
const theme = await magpie.ready;
|
|
12
27
|
let lang = theme.lang;
|
|
13
28
|
|
|
@@ -24,6 +39,10 @@
|
|
|
24
39
|
sort: V.SORTS.includes(opts.sort) ? opts.sort : "tokens",
|
|
25
40
|
summary: null,
|
|
26
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,
|
|
27
46
|
};
|
|
28
47
|
|
|
29
48
|
function words() {
|
|
@@ -42,9 +61,83 @@
|
|
|
42
61
|
});
|
|
43
62
|
}
|
|
44
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);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
45
136
|
function paint() {
|
|
46
137
|
pick("periods", "period", state.period);
|
|
47
138
|
pick("sorts", "sort", state.sort);
|
|
139
|
+
// mbridge's part does not depend on magpie's usage
|
|
140
|
+
paintMbridge();
|
|
48
141
|
const s = state.summary;
|
|
49
142
|
if (!s) return;
|
|
50
143
|
$("since").textContent = V.since(s, lang);
|
|
@@ -98,16 +191,55 @@
|
|
|
98
191
|
}
|
|
99
192
|
}
|
|
100
193
|
|
|
101
|
-
|
|
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
|
+
// what we write back must be what we just read: writing a stale
|
|
200
|
+
// copy would put an old snapshot back
|
|
201
|
+
opts = o;
|
|
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;
|
|
205
|
+
} catch {
|
|
206
|
+
// the options are not readable: keep what we have
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// loadUsage reads what magpie recorded; it moves slowly.
|
|
211
|
+
async function loadUsage() {
|
|
102
212
|
try {
|
|
103
213
|
state.summary = await magpie.read("usage:summary", { period: state.period });
|
|
104
214
|
state.error = null;
|
|
105
215
|
} catch (e) {
|
|
106
216
|
state.error = (e && e.message) || String(e);
|
|
107
217
|
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function load() {
|
|
221
|
+
await readOptions();
|
|
222
|
+
await loadUsage();
|
|
108
223
|
paint();
|
|
109
224
|
}
|
|
110
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 };
|
|
232
|
+
try {
|
|
233
|
+
const o = (await magpie.options.get()) || {};
|
|
234
|
+
o.cmd = cmd;
|
|
235
|
+
await magpie.options.set(o);
|
|
236
|
+
} catch (e) {
|
|
237
|
+
// mbridge will not hear it; the page says so
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
111
243
|
// magpie takes one options write a second: a quick second click is
|
|
112
244
|
// saved with the next one
|
|
113
245
|
let saving = null;
|
|
@@ -116,6 +248,8 @@
|
|
|
116
248
|
saving = setTimeout(async () => {
|
|
117
249
|
saving = null;
|
|
118
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
|
|
119
253
|
opts = (await magpie.options.set({ ...opts, period: state.period, sort: state.sort })) || opts;
|
|
120
254
|
} catch {
|
|
121
255
|
// not remembering is not worth an error on the page
|
|
@@ -147,7 +281,34 @@
|
|
|
147
281
|
b.textContent = V.sortLabel(b.dataset.sort, lang);
|
|
148
282
|
}
|
|
149
283
|
$("addr").textContent = V.STATUS_URL;
|
|
284
|
+
$("mbridge-ask").textContent = V.text(lang).mbridgeAsk;
|
|
285
|
+
$("mbridge-restart").textContent = V.text(lang).mbridgeRestart;
|
|
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
|
+
});
|
|
150
305
|
words();
|
|
151
306
|
await load();
|
|
152
|
-
|
|
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);
|
|
153
314
|
})();
|
package/page/index.html
CHANGED
|
@@ -47,6 +47,32 @@
|
|
|
47
47
|
<p class="muted empty" id="empty" hidden></p>
|
|
48
48
|
</section>
|
|
49
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
|
+
|
|
50
76
|
<section class="notes">
|
|
51
77
|
<p class="note" data-t="disclaimer"></p>
|
|
52
78
|
<p class="note">
|
package/page/style.css
CHANGED
|
@@ -46,3 +46,22 @@ h1 { margin: 0; font-size: 1.05em; font-weight: 600; }
|
|
|
46
46
|
.addr { user-select: all; color: var(--magpie-fg, inherit); }
|
|
47
47
|
.cmd { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--magpie-fg, inherit); }
|
|
48
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
|
@@ -33,6 +33,32 @@
|
|
|
33
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
34
|
status: "mbridge's own status:",
|
|
35
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",
|
|
36
62
|
},
|
|
37
63
|
zh: {
|
|
38
64
|
title: "mbridge",
|
|
@@ -58,6 +84,32 @@
|
|
|
58
84
|
"这是 magpie 记下的全部用量。客户端都走 mbridge 时它才等于 mbridge 的用量;你直接用 magpie 的那部分也在里面。",
|
|
59
85
|
status: "mbridge 自己的状态:",
|
|
60
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: "还没有判档",
|
|
61
113
|
},
|
|
62
114
|
};
|
|
63
115
|
|
|
@@ -155,6 +207,73 @@
|
|
|
155
207
|
return t["sort" + sort.charAt(0).toUpperCase() + sort.slice(1)] || sort;
|
|
156
208
|
}
|
|
157
209
|
|
|
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
|
+
|
|
158
277
|
globalThis.MbridgeView = {
|
|
159
278
|
TEXT,
|
|
160
279
|
PERIODS,
|
|
@@ -169,5 +288,6 @@
|
|
|
169
288
|
since,
|
|
170
289
|
periodLabel,
|
|
171
290
|
sortLabel,
|
|
291
|
+
mbridgeView,
|
|
172
292
|
};
|
|
173
293
|
})();
|