@weiping/mbridge-page 0.1.1 → 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 +1 -1
- package/page/app.js +147 -0
- 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,7 +191,21 @@
|
|
|
98
191
|
}
|
|
99
192
|
}
|
|
100
193
|
|
|
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
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
101
207
|
async function load() {
|
|
208
|
+
await readOptions();
|
|
102
209
|
try {
|
|
103
210
|
state.summary = await magpie.read("usage:summary", { period: state.period });
|
|
104
211
|
state.error = null;
|
|
@@ -108,6 +215,23 @@
|
|
|
108
215
|
paint();
|
|
109
216
|
}
|
|
110
217
|
|
|
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;
|
|
233
|
+
}
|
|
234
|
+
|
|
111
235
|
// magpie takes one options write a second: a quick second click is
|
|
112
236
|
// saved with the next one
|
|
113
237
|
let saving = null;
|
|
@@ -116,6 +240,8 @@
|
|
|
116
240
|
saving = setTimeout(async () => {
|
|
117
241
|
saving = null;
|
|
118
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
|
|
119
245
|
opts = (await magpie.options.set({ ...opts, period: state.period, sort: state.sort })) || opts;
|
|
120
246
|
} catch {
|
|
121
247
|
// not remembering is not worth an error on the page
|
|
@@ -147,6 +273,27 @@
|
|
|
147
273
|
b.textContent = V.sortLabel(b.dataset.sort, lang);
|
|
148
274
|
}
|
|
149
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;
|
|
296
|
+
});
|
|
150
297
|
words();
|
|
151
298
|
await load();
|
|
152
299
|
setInterval(load, 60000);
|
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
|
})();
|