@weiping/mbridge-page 0.3.0 → 0.5.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 +6 -6
- package/page/app.js +90 -166
- package/page/index.html +2 -44
- package/page/style.css +110 -64
- package/page/view.js +682 -186
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weiping/mbridge-page",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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,12 +23,12 @@
|
|
|
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"
|
package/page/app.js
CHANGED
|
@@ -1,191 +1,115 @@
|
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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)));
|
|
8
|
+
const theme = await magpie.ready;
|
|
9
|
+
let lang = "en";
|
|
10
|
+
if (theme && theme.lang) {
|
|
11
|
+
if (theme.lang.toLowerCase().startsWith("zh")) {
|
|
12
|
+
lang = "zh";
|
|
23
13
|
}
|
|
24
|
-
|
|
14
|
+
} else if (typeof navigator !== "undefined" && (navigator.language || "").toLowerCase().startsWith("zh")) {
|
|
15
|
+
lang = "zh";
|
|
25
16
|
}
|
|
26
|
-
const theme = await magpie.ready;
|
|
27
|
-
let lang = theme.lang;
|
|
28
17
|
|
|
29
|
-
// the page's own keys in the options (kept so mbridge's key survives
|
|
30
|
-
// a write)
|
|
31
18
|
let opts = {};
|
|
32
|
-
|
|
33
|
-
opts = (await magpie.options.get()) || {};
|
|
34
|
-
} catch {
|
|
35
|
-
// no options yet: mbridge publishes them
|
|
36
|
-
}
|
|
37
|
-
const state = {
|
|
38
|
-
mbridge: null,
|
|
39
|
-
cmdSeq: 0,
|
|
40
|
-
};
|
|
19
|
+
const current = { window: "7d" };
|
|
41
20
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
$("cmd").textContent = "mbridge service restart";
|
|
52
|
-
$("addr").textContent = V.STATUS_URL;
|
|
53
|
-
$("mbridge-ask").textContent = t.mbridgeAsk;
|
|
54
|
-
$("mbridge-restart").textContent = t.mbridgeRestart;
|
|
21
|
+
function fail(message) {
|
|
22
|
+
const root = document.getElementById("root");
|
|
23
|
+
root.textContent = "";
|
|
24
|
+
const h1 = document.createElement("h1");
|
|
25
|
+
h1.textContent = "鹊桥";
|
|
26
|
+
const banner = document.createElement("div");
|
|
27
|
+
banner.className = "banner bad err";
|
|
28
|
+
banner.textContent = message;
|
|
29
|
+
root.append(h1, banner);
|
|
55
30
|
}
|
|
56
31
|
|
|
57
|
-
|
|
58
|
-
// back must be what we just read, so mbridge's snapshot is not put back
|
|
59
|
-
// stale.
|
|
60
|
-
async function readOptions() {
|
|
32
|
+
async function command(id) {
|
|
61
33
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
34
|
+
const o = (await magpie.options.get()) || {};
|
|
35
|
+
o.cmd = { id: id, seq: Date.now() };
|
|
36
|
+
await magpie.options.set(o);
|
|
37
|
+
return true;
|
|
64
38
|
} catch {
|
|
65
|
-
|
|
39
|
+
return false;
|
|
66
40
|
}
|
|
67
41
|
}
|
|
68
42
|
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const cell = document.createElement("div");
|
|
86
|
-
cell.className = "stat";
|
|
87
|
-
const b = document.createElement("b");
|
|
88
|
-
b.textContent = value;
|
|
89
|
-
const s2 = document.createElement("span");
|
|
90
|
-
s2.textContent = label;
|
|
91
|
-
cell.append(b, s2);
|
|
92
|
-
stats.append(cell);
|
|
43
|
+
async function setWindow(kind) {
|
|
44
|
+
current.window = kind;
|
|
45
|
+
try {
|
|
46
|
+
const o = (await magpie.options.get()) || {};
|
|
47
|
+
o.period = kind;
|
|
48
|
+
await magpie.options.set(o);
|
|
49
|
+
// Ask mbridge to immediately publish the updated window
|
|
50
|
+
await command("refresh");
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readOptions() {
|
|
55
|
+
try {
|
|
56
|
+
opts = (await magpie.options.get()) || {};
|
|
57
|
+
if (opts.period && (opts.period === "1d" || opts.period === "7d" || opts.period === "30d")) {
|
|
58
|
+
current.window = opts.period;
|
|
93
59
|
}
|
|
60
|
+
return opts.mbridge || null;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return null;
|
|
94
63
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (c.reason) bits.push(c.reason);
|
|
105
|
-
if (c.conf !== undefined && c.conf !== null) bits.push("conf " + c.conf.toFixed(2));
|
|
106
|
-
if (c.tools !== null && c.tools !== undefined) bits.push(t.tools + " " + c.tools);
|
|
107
|
-
live.append(el("div", { class: "card" }, [
|
|
108
|
-
el("div", { class: "row1" }, [
|
|
109
|
-
el("span", { text: c.harness + " · " + c.agent }),
|
|
110
|
-
el("span", { text: c.tier + (c.group ? " → " + c.group : "") }),
|
|
111
|
-
]),
|
|
112
|
-
el("div", { class: "row2", text: c.session + " · " + bits.join(" · ") }),
|
|
113
|
-
]));
|
|
114
|
-
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderView(data) {
|
|
67
|
+
// When mbridge is not running or not installed (empty options or no valid snapshot with a running pid):
|
|
68
|
+
// Display the setup and installation guide instead of empty dashboard!
|
|
69
|
+
const isLive = data && data.pid && (data.service ? data.service.running !== false : true);
|
|
70
|
+
if (!isLive) {
|
|
71
|
+
V.renderGuide(null, lang);
|
|
72
|
+
return;
|
|
115
73
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
];
|
|
129
|
-
for (const [text, cls] of cells) {
|
|
130
|
-
const td = document.createElement("td");
|
|
131
|
-
if (cls) td.className = cls;
|
|
132
|
-
td.textContent = text;
|
|
133
|
-
tr.append(td);
|
|
74
|
+
|
|
75
|
+
V.render(data);
|
|
76
|
+
|
|
77
|
+
// Wire up tab switching in the rendered view
|
|
78
|
+
const tabButtons = document.querySelectorAll(".tabs button");
|
|
79
|
+
tabButtons.forEach((btn) => {
|
|
80
|
+
const kind = btn.textContent.trim();
|
|
81
|
+
if (kind === "1d" || kind === "7d" || kind === "30d") {
|
|
82
|
+
btn.onclick = async () => {
|
|
83
|
+
await setWindow(kind);
|
|
84
|
+
tabButtons.forEach((b) => b.setAttribute("aria-pressed", String(b.textContent.trim() === kind)));
|
|
85
|
+
};
|
|
134
86
|
}
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
}
|
|
87
|
+
});
|
|
138
88
|
|
|
139
|
-
|
|
140
|
-
|
|
89
|
+
// Wire up the restart button in the rendered view
|
|
90
|
+
const restartBtn = document.querySelector(".restart");
|
|
91
|
+
if (restartBtn) {
|
|
92
|
+
restartBtn.onclick = async () => {
|
|
93
|
+
restartBtn.disabled = true;
|
|
94
|
+
restartBtn.textContent = "正在重启…";
|
|
95
|
+
const ok = await command("restart");
|
|
96
|
+
if (ok) {
|
|
97
|
+
restartBtn.textContent = "已发出重启指令";
|
|
98
|
+
} else {
|
|
99
|
+
restartBtn.textContent = "指令发送失败";
|
|
100
|
+
restartBtn.disabled = false;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
141
104
|
}
|
|
142
105
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
async function command(id) {
|
|
147
|
-
state.cmdSeq += 1;
|
|
148
|
-
const cmd = { id: id, seq: state.cmdSeq };
|
|
149
|
-
try {
|
|
150
|
-
const o = (await magpie.options.get()) || {};
|
|
151
|
-
o.cmd = cmd;
|
|
152
|
-
await magpie.options.set(o);
|
|
153
|
-
} catch {
|
|
154
|
-
// mbridge will not hear it; the page says so
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
return true;
|
|
106
|
+
async function poll() {
|
|
107
|
+
const snap = await readOptions();
|
|
108
|
+
renderView(snap);
|
|
158
109
|
}
|
|
159
110
|
|
|
160
|
-
|
|
161
|
-
$("mbridge-restart").textContent = V.text(lang).mbridgeRestart;
|
|
162
|
-
$("mbridge-ask").addEventListener("click", async () => {
|
|
163
|
-
const b = $("mbridge-ask");
|
|
164
|
-
const t = V.text(lang);
|
|
165
|
-
b.disabled = true;
|
|
166
|
-
b.textContent = t.mbridgeAsking;
|
|
167
|
-
const ok = await command("refresh");
|
|
168
|
-
if (!ok) { b.disabled = false; b.textContent = t.mbridgeAsk; }
|
|
169
|
-
// mbridge publishes and the next poll picks it up
|
|
170
|
-
setTimeout(() => { b.disabled = false; b.textContent = t.mbridgeAsk; load(); }, 3000);
|
|
171
|
-
});
|
|
172
|
-
$("mbridge-restart").addEventListener("click", async () => {
|
|
173
|
-
const b = $("mbridge-restart");
|
|
174
|
-
const t = V.text(lang);
|
|
175
|
-
b.disabled = true;
|
|
176
|
-
b.textContent = t.mbridgeRestarting;
|
|
177
|
-
const ok = await command("restart");
|
|
178
|
-
if (!ok) { b.disabled = false; b.textContent = t.mbridgeRestart; return; }
|
|
179
|
-
b.textContent = t.mbridgeRestartAsked;
|
|
180
|
-
});
|
|
111
|
+
globalThis.pollMbridge = poll;
|
|
181
112
|
|
|
182
|
-
|
|
183
|
-
|
|
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);
|
|
113
|
+
await poll();
|
|
114
|
+
setInterval(poll, 5000);
|
|
191
115
|
})();
|
package/page/index.html
CHANGED
|
@@ -3,55 +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"></p>
|
|
16
|
-
</header>
|
|
17
|
-
|
|
18
|
-
<section class="mbridge">
|
|
19
|
-
<div class="mbridge-head">
|
|
20
|
-
<h2 data-t="mbridge">mbridge</h2>
|
|
21
|
-
<span id="mbridge-age" class="muted"></span>
|
|
22
|
-
</div>
|
|
23
|
-
<div id="mbridge-stats" class="stats"></div>
|
|
24
|
-
<h3 data-t="mbridgeLive">sessions running now</h3>
|
|
25
|
-
<div id="mbridge-live" class="cards"></div>
|
|
26
|
-
<h3 data-t="mbridgeDecisions">recent decisions</h3>
|
|
27
|
-
<table class="table mbridge-table">
|
|
28
|
-
<thead>
|
|
29
|
-
<tr>
|
|
30
|
-
<th></th><th data-t="harness">harness</th><th data-t="agent">agent</th>
|
|
31
|
-
<th data-t="tier">tier</th><th data-t="advised">advised</th>
|
|
32
|
-
<th data-t="reason">reason</th><th class="num" data-t="conf">confidence</th>
|
|
33
|
-
</tr>
|
|
34
|
-
</thead>
|
|
35
|
-
<tbody id="mbridge-decisions"></tbody>
|
|
36
|
-
</table>
|
|
37
|
-
<p id="mbridge-empty" class="muted"></p>
|
|
38
|
-
<div class="mbridge-actions">
|
|
39
|
-
<button type="button" id="mbridge-ask" class="btn"></button>
|
|
40
|
-
<button type="button" id="mbridge-restart" class="btn"></button>
|
|
41
|
-
</div>
|
|
42
|
-
</section>
|
|
43
|
-
|
|
44
|
-
<section class="notes">
|
|
45
|
-
<p class="note">
|
|
46
|
-
<span id="status"></span>
|
|
47
|
-
<span class="addr" id="addr" title="select and copy it"></span>
|
|
48
|
-
<span id="restart"></span>
|
|
49
|
-
<code class="cmd" id="cmd"></code>
|
|
50
|
-
</p>
|
|
51
|
-
</section>
|
|
52
|
-
|
|
53
|
-
<p class="error" id="error" hidden></p>
|
|
54
|
-
</main>
|
|
12
|
+
<main id="root"></main>
|
|
55
13
|
<script src="app.js" defer></script>
|
|
56
14
|
</body>
|
|
57
15
|
</html>
|
package/page/style.css
CHANGED
|
@@ -1,67 +1,113 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
--bg: var(--magpie-bg, #ffffff); --fg: var(--magpie-fg, #1a1a1a); --dim: var(--magpie-fg-muted, #6b6b6b); --line: var(--magpie-border, #e3e3e3);
|
|
4
|
+
--ok: #1a7f37; --warn: #9a6700; --bad: #b42318; --chip: var(--magpie-bg-subtle, #f2f2f2);
|
|
5
|
+
}
|
|
6
|
+
@media (prefers-color-scheme: dark) {
|
|
7
|
+
:root { --bg: #17181a; --fg: #e8e8e8; --dim: #9a9a9a; --line: #2e2f33;
|
|
8
|
+
--ok: #57ab5a; --warn: #d0a44a; --bad: #e5534b; --chip: #232427; }
|
|
9
|
+
}
|
|
10
|
+
* { box-sizing: border-box; }
|
|
11
|
+
body { margin: 0; padding: 1.25rem; background: var(--bg); color: var(--fg);
|
|
12
|
+
font: 13px/1.5 var(--magpie-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif); }
|
|
13
|
+
h1 { font-size: 1.1rem; margin: 0 0 .2rem; }
|
|
14
|
+
h2 { font-size: .85rem; margin: 1.4rem 0 .5rem; text-transform: uppercase;
|
|
15
|
+
letter-spacing: .06em; color: var(--dim); }
|
|
16
|
+
.sub { color: var(--dim); margin: 0 0 1rem; }
|
|
17
|
+
.banner { padding: .5rem .7rem; border-radius: 6px; margin: .6rem 0;
|
|
18
|
+
background: color-mix(in srgb, var(--warn) 14%, transparent);
|
|
19
|
+
border: 1px solid color-mix(in srgb, var(--warn) 45%, transparent); }
|
|
20
|
+
.banner.bad { background: color-mix(in srgb, var(--bad) 12%, transparent);
|
|
21
|
+
border-color: color-mix(in srgb, var(--bad) 45%, transparent); }
|
|
22
|
+
table { border-collapse: collapse; width: 100%; }
|
|
23
|
+
th, td { text-align: left; padding: .3rem .5rem; border-bottom: 1px solid var(--line);
|
|
24
|
+
vertical-align: top; }
|
|
25
|
+
th { color: var(--dim); font-weight: 500; white-space: nowrap; }
|
|
26
|
+
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
27
|
+
.cards { display: grid; gap: .5rem; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
|
|
28
|
+
.card { border: 1px solid var(--line); border-radius: 8px; padding: .6rem .7rem; background: var(--chip); }
|
|
29
|
+
.card .row1 { display: flex; justify-content: space-between; gap: .5rem; font-weight: 600; }
|
|
30
|
+
.card .row2 { color: var(--dim); font-size: .85em; margin-top: .15rem;
|
|
31
|
+
overflow-wrap: anywhere; }
|
|
32
|
+
.card .row3 { margin-top: .3rem; }
|
|
33
|
+
.card.off { opacity: .5; }
|
|
34
|
+
.chip { display: inline-block; padding: .05rem .4rem; border-radius: 4px;
|
|
35
|
+
background: var(--chip); border: 1px solid var(--line); font-size: .85em; }
|
|
36
|
+
.chip.tier-fast { border-color: var(--ok); }
|
|
37
|
+
.chip.tier-balanced { border-color: var(--warn); }
|
|
38
|
+
.chip.tier-performance { border-color: var(--bad); }
|
|
39
|
+
.muted { color: var(--dim); }
|
|
40
|
+
.off { color: var(--dim); }
|
|
41
|
+
.bar { height: 4px; background: var(--line); border-radius: 2px; overflow: hidden; }
|
|
42
|
+
.bar > i { display: block; height: 100%; background: var(--fg); }
|
|
43
|
+
.err { color: var(--bad); }
|
|
44
|
+
.tabs { display: flex; gap: .3rem; margin: 0 0 .8rem; }
|
|
45
|
+
.tabs button { font: inherit; padding: .2rem .55rem; border-radius: 5px; cursor: pointer;
|
|
46
|
+
border: 1px solid var(--line); background: var(--bg); color: var(--fg); }
|
|
47
|
+
.tabs button[aria-pressed="true"] { background: var(--fg); color: var(--bg); }
|
|
48
|
+
.kv { display: grid; grid-template-columns: max-content 1fr; gap: .15rem .8rem; }
|
|
49
|
+
.kv > :nth-child(odd) { color: var(--dim); }
|
|
50
|
+
.foot { margin-top: 2rem; color: var(--dim); font-size: .85em; }
|
|
51
|
+
.foot ul { margin: .3rem 0; padding-left: 1.1rem; }
|
|
52
|
+
|
|
53
|
+
.head { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
|
|
54
|
+
h1 { font-size: 1.15rem; margin: 0; }
|
|
55
|
+
h1 .version { color: var(--dim); font-weight: 400; font-size: .8em; }
|
|
56
|
+
h3 { font-size: .8rem; margin: .8rem 0 .3rem; color: var(--dim); font-weight: 500; }
|
|
57
|
+
.pulse { display: flex; gap: 1.4rem; align-items: baseline; flex-wrap: wrap;
|
|
58
|
+
margin: .2rem 0 .9rem; padding: .55rem .7rem; border-radius: 8px;
|
|
59
|
+
border: 1px solid var(--line); background: var(--chip); }
|
|
60
|
+
.pulse .stat { display: flex; flex-direction: column; }
|
|
61
|
+
.pulse .stat b { font-size: 1.25em; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
62
|
+
.pulse .stat span { color: var(--dim); font-size: .8em; }
|
|
63
|
+
.pulse .tabs { margin-left: auto; }
|
|
64
|
+
.procline { display: flex; gap: 1rem; flex-wrap: wrap; color: var(--dim);
|
|
65
|
+
font-size: .85em; margin-bottom: .4rem; }
|
|
66
|
+
.procline span { white-space: nowrap; }
|
|
67
|
+
.dists { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
|
|
68
|
+
.distrow { display: grid; grid-template-columns: 8rem 1fr auto; gap: .5rem;
|
|
69
|
+
align-items: center; padding: 1px 0; }
|
|
70
|
+
.distname { overflow-wrap: anywhere; font-size: .9em; }
|
|
71
|
+
.distnum { font-variant-numeric: tabular-nums; font-size: .85em; color: var(--dim); }
|
|
72
|
+
.fixes { margin: .3rem 0 .6rem; padding-left: 1.2rem; color: var(--dim); }
|
|
73
|
+
.cal { margin: .5rem 0; }
|
|
74
|
+
.cfg { margin-top: 1.2rem; border-top: 1px solid var(--line); padding-top: .6rem; }
|
|
75
|
+
.cfg summary { cursor: pointer; color: var(--dim); }
|
|
76
|
+
.restartrow { margin-top: 1rem; }
|
|
77
|
+
.restart { font: inherit; padding: .3rem .8rem; border-radius: 6px; cursor: pointer;
|
|
78
|
+
border: 1px solid var(--line); background: var(--bg); color: var(--fg); }
|
|
79
|
+
.restart[disabled] { opacity: .6; cursor: default; }
|
|
80
|
+
table.wide { font-size: .92em; }
|
|
81
|
+
table.wide td { padding: .2rem .45rem; }
|
|
82
|
+
td.reason { color: var(--dim); }
|
|
83
|
+
td.differs { color: var(--fg); font-weight: 600; }
|
|
84
|
+
.chip.tier-fast { border-color: var(--ok); }
|
|
85
|
+
.chip.tier-balanced { border-color: var(--warn); }
|
|
86
|
+
.chip.tier-performance { border-color: var(--bad); }
|
|
87
|
+
.chip.differs { border-color: var(--fg); }
|
|
88
|
+
|
|
89
|
+
/* Setup guide view (when mbridge is not running or not installed) */
|
|
90
|
+
.setup-guide { max-width: 760px; margin: 0 auto; padding: 1rem 0; }
|
|
91
|
+
.guide-card {
|
|
92
|
+
border: 1px solid var(--line); border-radius: 8px;
|
|
93
|
+
padding: 1.25rem; margin-bottom: 1.25rem; background: var(--chip);
|
|
19
94
|
}
|
|
20
|
-
.
|
|
21
|
-
|
|
95
|
+
.guide-card h2 { margin: 0 0 .5rem; font-size: 1rem; color: var(--fg); text-transform: none; letter-spacing: normal; }
|
|
96
|
+
.guide-desc { color: var(--dim); margin: 0 0 1rem; font-size: .9em; }
|
|
97
|
+
.guide-steps { display: flex; flex-direction: column; gap: 1rem; }
|
|
98
|
+
.guide-step { display: flex; flex-direction: column; gap: .3rem; }
|
|
99
|
+
.step-title { font-weight: 600; font-size: .9em; }
|
|
100
|
+
.step-cmd { position: relative; display: flex; align-items: center; }
|
|
101
|
+
.step-cmd pre {
|
|
102
|
+
margin: 0; padding: .6rem .8rem; background: var(--bg); border: 1px solid var(--line);
|
|
103
|
+
border-radius: 6px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
104
|
+
font-size: .88em; width: 100%; overflow-x: auto; color: var(--fg);
|
|
22
105
|
}
|
|
23
|
-
.
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
106
|
+
.guide-tip { margin-top: .6rem; font-size: .85em; color: var(--dim); }
|
|
107
|
+
.guide-tip a { color: var(--ok); text-decoration: none; }
|
|
108
|
+
.guide-refresh {
|
|
109
|
+
display: inline-flex; align-items: center; gap: .5rem;
|
|
110
|
+
padding: .4rem .9rem; border-radius: 6px; border: 1px solid var(--line);
|
|
111
|
+
background: var(--bg); color: var(--fg); cursor: pointer; font: inherit;
|
|
28
112
|
}
|
|
29
|
-
.
|
|
30
|
-
.tile .v { font-size: 1.15em; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
31
|
-
.tile .s { color: var(--magpie-fg-muted, #6b6b6b); }
|
|
32
|
-
.models h2 { margin: 0 0 4px; font-size: 1em; font-weight: 600; }
|
|
33
|
-
.muted { color: var(--magpie-fg-muted, #6b6b6b); }
|
|
34
|
-
.table { width: 100%; border-collapse: collapse; }
|
|
35
|
-
.table th, .table td { text-align: left; padding: 3px 4px; border-bottom: 1px solid var(--magpie-border, #e3e3e3); }
|
|
36
|
-
.table .num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
37
|
-
.table th.num { text-align: right; color: var(--magpie-fg-muted, #6b6b6b); font-weight: 500; }
|
|
38
|
-
.model { width: 55%; overflow-wrap: anywhere; }
|
|
39
|
-
.bar { display: block; height: 2px; margin-top: 2px; background: var(--magpie-border, #e3e3e3); border-radius: 1px; overflow: hidden; }
|
|
40
|
-
.bar > i { display: block; height: 100%; background: var(--magpie-accent, #2f6feb); }
|
|
41
|
-
.empty { margin: 6px 0; }
|
|
42
|
-
.notes { margin-top: 14px; }
|
|
43
|
-
.note { margin: 4px 0; color: var(--magpie-fg-muted, #6b6b6b); }
|
|
44
|
-
/* the address is selectable text, not a link: the sandbox may navigate
|
|
45
|
-
itself, but that is a one-way ticket (SP14 §2) */
|
|
46
|
-
.addr { user-select: all; color: var(--magpie-fg, inherit); }
|
|
47
|
-
.cmd { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--magpie-fg, inherit); }
|
|
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; }
|
|
113
|
+
.guide-refresh:hover { background: var(--chip); }
|