@weiping/mbridge-page 0.2.0 → 0.3.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.0",
3
+ "version": "0.3.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,9 +33,7 @@
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,7 +1,7 @@
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).
1
+ // app.js reads what mbridge published into the plugin's options, and
2
+ // draws it. It never reaches mbridge: the sandbox a magpie page runs in
3
+ // has no network at all (connect-src 'none'), so mbridge pushes its
4
+ // state into the options and the page reads them in-process (SP14 §7).
5
5
  //
6
6
  // A classic script, like the community's usage page: it reads the view
7
7
  // from globalThis.
@@ -26,25 +26,21 @@
26
26
  const theme = await magpie.ready;
27
27
  let lang = theme.lang;
28
28
 
29
- // the period and the sort live in this plugin's own options, so the
30
- // page opens where it was left
29
+ // the page's own keys in the options (kept so mbridge's key survives
30
+ // a write)
31
31
  let opts = {};
32
32
  try {
33
33
  opts = (await magpie.options.get()) || {};
34
34
  } catch {
35
- // no options yet: the defaults in package.json stand
35
+ // no options yet: mbridge publishes them
36
36
  }
37
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
38
  mbridge: null,
44
- // the sequence number of the last command the page sent
45
39
  cmdSeq: 0,
46
40
  };
47
41
 
42
+ // the address of mbridge's own page: selectable text, not a link (the
43
+ // sandbox may navigate itself, but that is a one-way ticket, SP14 §2)
48
44
  function words() {
49
45
  const t = V.text(lang);
50
46
  document.querySelectorAll("[data-t]").forEach((e) => {
@@ -53,16 +49,23 @@
53
49
  $("status").textContent = t.status;
54
50
  $("restart").textContent = t.restart;
55
51
  $("cmd").textContent = "mbridge service restart";
52
+ $("addr").textContent = V.STATUS_URL;
53
+ $("mbridge-ask").textContent = t.mbridgeAsk;
54
+ $("mbridge-restart").textContent = t.mbridgeRestart;
56
55
  }
57
56
 
58
- function pick(group, attr, value) {
59
- $(group).querySelectorAll("button").forEach((b) => {
60
- b.setAttribute("aria-selected", String(b.dataset[attr] === value));
61
- });
57
+ // readOptions reads the options: what mbridge published. What we write
58
+ // back must be what we just read, so mbridge's snapshot is not put back
59
+ // stale.
60
+ async function readOptions() {
61
+ try {
62
+ opts = (await magpie.options.get()) || {};
63
+ state.mbridge = opts.mbridge || null;
64
+ } catch {
65
+ // the options are not readable: keep what we have
66
+ }
62
67
  }
63
68
 
64
- // paintMbridge draws what mbridge published: its numbers, the sessions
65
- // running now, and the decisions it made.
66
69
  function paintMbridge() {
67
70
  const t = V.text(lang);
68
71
  const v = V.mbridgeView(state.mbridge, lang);
@@ -134,85 +137,7 @@
134
137
  }
135
138
 
136
139
  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
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
- );
159
-
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
- }
192
- }
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
-
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();
216
141
  }
217
142
 
218
143
  // command asks mbridge to do something. The sequence number makes it
@@ -225,54 +150,13 @@
225
150
  const o = (await magpie.options.get()) || {};
226
151
  o.cmd = cmd;
227
152
  await magpie.options.set(o);
228
- } catch (e) {
153
+ } catch {
229
154
  // mbridge will not hear it; the page says so
230
155
  return false;
231
156
  }
232
157
  return true;
233
158
  }
234
159
 
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);
250
- }
251
-
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
160
  $("mbridge-ask").textContent = V.text(lang).mbridgeAsk;
277
161
  $("mbridge-restart").textContent = V.text(lang).mbridgeRestart;
278
162
  $("mbridge-ask").addEventListener("click", async () => {
@@ -294,7 +178,14 @@
294
178
  if (!ok) { b.disabled = false; b.textContent = t.mbridgeRestart; return; }
295
179
  b.textContent = t.mbridgeRestartAsked;
296
180
  });
181
+
297
182
  words();
298
- await load();
299
- setInterval(load, 60000);
183
+ await readOptions();
184
+ paint();
185
+ // mbridge's snapshot moves every fifteen seconds, and reading the
186
+ // options is free
187
+ setInterval(async () => {
188
+ await readOptions();
189
+ paint();
190
+ }, 5000);
300
191
  })();
package/page/index.html CHANGED
@@ -12,41 +12,9 @@
12
12
  <main class="root">
13
13
  <header class="head">
14
14
  <h1 data-t="title">mbridge</h1>
15
- <p class="subtitle" data-t="subtitle"></p>
16
- <p class="since" id="since"></p>
15
+ <p class="subtitle"></p>
17
16
  </header>
18
17
 
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
18
  <section class="mbridge">
51
19
  <div class="mbridge-head">
52
20
  <h2 data-t="mbridge">mbridge</h2>
@@ -74,7 +42,6 @@
74
42
  </section>
75
43
 
76
44
  <section class="notes">
77
- <p class="note" data-t="disclaimer"></p>
78
45
  <p class="note">
79
46
  <span id="status"></span>
80
47
  <span class="addr" id="addr" title="select and copy it"></span>
package/page/view.js CHANGED
@@ -12,25 +12,9 @@
12
12
  en: {
13
13
  title: "mbridge",
14
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
15
  today: "today",
23
16
  "7d": "7 days",
24
17
  "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
18
  status: "mbridge's own status:",
35
19
  restart: "restart it with",
36
20
  mbridge: "mbridge",
@@ -63,25 +47,9 @@
63
47
  zh: {
64
48
  title: "mbridge",
65
49
  subtitle: "magpie 记下的全部用量",
66
- calls: "调用",
67
- errors: "错误",
68
- errorShare: "占调用",
69
- tokens: "Token",
70
- cost: "费用",
71
- models: "按模型",
72
- model: "模型",
73
50
  today: "今天",
74
51
  "7d": "7 天",
75
52
  "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
53
  status: "mbridge 自己的状态:",
86
54
  restart: "重启用",
87
55
  mbridge: "mbridge",
@@ -113,8 +81,6 @@
113
81
  },
114
82
  };
115
83
 
116
- const PERIODS = ["today", "7d", "30d"];
117
- const SORTS = ["tokens", "calls", "cost"];
118
84
  const STATUS_URL = "http://127.0.0.1:3426/status";
119
85
 
120
86
  // text is the words for a language: zh for anything that starts with zh.
@@ -153,65 +119,13 @@
153
119
  return (Math.round(v * 10) / 10).toString() + "%";
154
120
  }
155
121
 
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
- }
172
-
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) => ({
183
- model: m.model || "?",
184
- calls: count(m.calls),
185
- tokens: count((m.input || 0) + (m.output || 0)),
186
- cost: money(m.cost),
187
- share: pct(value(m), total),
188
- shareValue: total > 0 ? value(m) / total : 0,
189
- }));
190
- }
191
-
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
- }
198
-
199
- // periodLabel is the name of a period.
200
- function periodLabel(period, lang) {
201
- return text(lang)[period] || period;
202
- }
203
-
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
- }
209
-
210
122
  // mbridgeView is what the page shows of mbridge, from the snapshot
211
123
  // mbridge published into the plugin's options (SP14 §7).
212
124
  function mbridgeView(snapshot, lang) {
213
125
  const t = text(lang);
214
- if (!snapshot) {
126
+ // nothing published, or an object that is not a snapshot: an empty
127
+ // options object is not a snapshot, it is no snapshot
128
+ if (!snapshot || (!snapshot.seq && !snapshot.version)) {
215
129
  return { none: true, text: t.mbridgeNone };
216
130
  }
217
131
  const s = snapshot;
@@ -276,18 +190,8 @@
276
190
 
277
191
  globalThis.MbridgeView = {
278
192
  TEXT,
279
- PERIODS,
280
- SORTS,
281
193
  STATUS_URL,
282
194
  text,
283
- count,
284
- money,
285
- pct,
286
- tiles,
287
- rows,
288
- since,
289
- periodLabel,
290
- sortLabel,
291
195
  mbridgeView,
292
196
  };
293
197
  })();