@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weiping/mbridge-page",
3
- "version": "0.2.1",
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": "mbridge",
26
+ "title": "鹊桥",
27
27
  "titles": {
28
- "zh": "mbridge",
29
- "zh-TW": "mbridge",
30
- "ja": "mbridge",
31
- "de": "mbridge"
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 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.
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
- let lang = theme.lang;
28
-
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
- };
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
- 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);
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
- 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;
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
- // readOptions reads the plugin's options: the page's period and sort,
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
- // 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;
32
+ o.cmd = { id: id, seq: Date.now() };
33
+ await magpie.options.set(o);
34
+ return true;
205
35
  } catch {
206
- // the options are not readable: keep what we have
36
+ return false;
207
37
  }
208
38
  }
209
39
 
210
- // loadUsage reads what magpie recorded; it moves slowly.
211
- async function loadUsage() {
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.cmd = cmd;
44
+ o.period = kind;
235
45
  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;
46
+ // Ask mbridge to immediately publish the updated window
47
+ await command("refresh");
48
+ } catch {}
241
49
  }
242
50
 
243
- // magpie takes one options write a second: a quick second click is
244
- // saved with the next one
245
- let saving = null;
246
- function save() {
247
- if (saving) return;
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
- }, 1100);
57
+ return opts.mbridge || null;
58
+ } catch (e) {
59
+ return null;
60
+ }
258
61
  }
259
62
 
260
- $("periods").addEventListener("click", (e) => {
261
- const b = e.target.closest("button[data-period]");
262
- if (!b || b.dataset.period === state.period) return;
263
- state.period = b.dataset.period;
264
- save();
265
- load();
266
- });
63
+ function renderView(data) {
64
+ if (!data) {
65
+ fail("鹊桥 正在等待 mbridge 发布快照数据…");
66
+ return;
67
+ }
68
+ V.render(data);
267
69
 
268
- $("sorts").addEventListener("click", (e) => {
269
- const b = e.target.closest("button[data-sort]");
270
- if (!b || b.dataset.sort === state.sort) return;
271
- state.sort = b.dataset.sort;
272
- save();
273
- paint();
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
- // the period and sort buttons carry their own words
277
- for (const b of $("periods").querySelectorAll("button[data-period]")) {
278
- b.textContent = V.periodLabel(b.dataset.period, lang);
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
- for (const b of $("sorts").querySelectorAll("button[data-sort]")) {
281
- b.textContent = V.sortLabel(b.dataset.sort, lang);
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
- $("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
- });
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>mbridge</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 class="root">
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>