@weiping/mbridge-page 0.3.0 → 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 +6 -6
- package/page/app.js +86 -166
- package/page/index.html +2 -44
- package/page/style.css +86 -66
- package/page/view.js +573 -186
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weiping/mbridge-page",
|
|
3
|
-
"version": "0.
|
|
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,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,111 @@
|
|
|
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
|
-
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)));
|
|
8
|
+
const theme = await magpie.ready;
|
|
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
|
|
23
12
|
}
|
|
24
|
-
return n;
|
|
25
13
|
}
|
|
26
|
-
const theme = await magpie.ready;
|
|
27
|
-
let lang = theme.lang;
|
|
28
14
|
|
|
29
|
-
// the page's own keys in the options (kept so mbridge's key survives
|
|
30
|
-
// a write)
|
|
31
15
|
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
|
-
};
|
|
16
|
+
const current = { window: "7d" };
|
|
41
17
|
|
|
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;
|
|
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);
|
|
55
27
|
}
|
|
56
28
|
|
|
57
|
-
|
|
58
|
-
// back must be what we just read, so mbridge's snapshot is not put back
|
|
59
|
-
// stale.
|
|
60
|
-
async function readOptions() {
|
|
29
|
+
async function command(id) {
|
|
61
30
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
31
|
+
const o = (await magpie.options.get()) || {};
|
|
32
|
+
o.cmd = { id: id, seq: Date.now() };
|
|
33
|
+
await magpie.options.set(o);
|
|
34
|
+
return true;
|
|
64
35
|
} catch {
|
|
65
|
-
|
|
36
|
+
return false;
|
|
66
37
|
}
|
|
67
38
|
}
|
|
68
39
|
|
|
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);
|
|
40
|
+
async function setWindow(kind) {
|
|
41
|
+
current.window = kind;
|
|
42
|
+
try {
|
|
43
|
+
const o = (await magpie.options.get()) || {};
|
|
44
|
+
o.period = kind;
|
|
45
|
+
await magpie.options.set(o);
|
|
46
|
+
// Ask mbridge to immediately publish the updated window
|
|
47
|
+
await command("refresh");
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
|
|
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;
|
|
93
56
|
}
|
|
57
|
+
return opts.mbridge || null;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
return null;
|
|
94
60
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
} else {
|
|
102
|
-
for (const c of v.live) {
|
|
103
|
-
const bits = [];
|
|
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
|
-
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderView(data) {
|
|
64
|
+
if (!data) {
|
|
65
|
+
fail("鹊桥 正在等待 mbridge 发布快照数据…");
|
|
66
|
+
return;
|
|
115
67
|
}
|
|
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);
|
|
68
|
+
V.render(data);
|
|
69
|
+
|
|
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
|
+
};
|
|
134
80
|
}
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
}
|
|
81
|
+
});
|
|
138
82
|
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
}
|
|
141
98
|
}
|
|
142
99
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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;
|
|
100
|
+
async function poll() {
|
|
101
|
+
const snap = await readOptions();
|
|
102
|
+
if (snap) {
|
|
103
|
+
renderView(snap);
|
|
104
|
+
} else {
|
|
105
|
+
fail("鹊桥 正在等待 mbridge 发布快照数据…");
|
|
156
106
|
}
|
|
157
|
-
return true;
|
|
158
107
|
}
|
|
159
108
|
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
});
|
|
181
|
-
|
|
182
|
-
words();
|
|
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);
|
|
109
|
+
await poll();
|
|
110
|
+
setInterval(poll, 5000);
|
|
191
111
|
})();
|
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,87 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
.
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
.
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
.
|
|
42
|
-
.
|
|
43
|
-
.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
48
|
-
.
|
|
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; }
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
.
|
|
56
|
-
|
|
57
|
-
.stat
|
|
58
|
-
.
|
|
59
|
-
.
|
|
60
|
-
.
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
.
|
|
64
|
-
.
|
|
65
|
-
.
|
|
66
|
-
|
|
67
|
-
.
|
|
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); }
|