|
7 | 7 | <title>pingping — link quality oscilloscope</title> |
8 | 8 | <style> |
9 | 9 | :root { |
10 | | - --bg:#0a0c0e; --panel:#12171a; --screen:#07090a; --line:#1f262b; |
11 | | - --phos:#7ce38b; --amber:#e8b44a; --alert:#ff5c57; --ink:#cdd6cf; --dim:#5f6f64; |
12 | | - --grid-axis:#2a332c; --grid-split:#141d16; |
13 | | - --smoke:rgba(124,227,139,.16); --lossbar:rgba(255,92,87,.35); --glow:6; |
14 | | - --smoke-hot:#eafff0; --smoke-mid:#57d573; --smoke-cool:#7d6a2e; |
15 | | - --shadow:0 1px 2px rgba(0,0,0,.5); --shadow-lg:0 10px 30px rgba(0,0,0,.45); |
16 | | - --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans", sans-serif; |
17 | | - --mono: ui-monospace, "SF Mono", "Cascadia Mono", Consolas, "Noto Sans Mono", monospace; |
| 10 | + --bg:#0a0c0e; --panel:#101613; --screen:#060808; --line:#1e242a; |
| 11 | + --phos:#7ce38b; --amber:#e8b44a; --alert:#ff5c57; --ink:#c7d0c9; --dim:#5c6b60; |
| 12 | + --grid-axis:#2a332c; --grid-split:#10281a; |
| 13 | + --smoke:rgba(124,227,139,.16); --lossbar:rgba(255,92,87,.55); --glow:6; |
| 14 | + --smoke-hot:#d8ffe2; --smoke-mid:#7ce38b; --smoke-cool:#8a7a3a; |
| 15 | + --mono: ui-monospace,"SF Mono","Cascadia Mono",Consolas,"Noto Sans Mono",monospace; |
18 | 16 | } |
| 17 | + /* Daylight: warm paper — the trace printed on lab graph paper */ |
19 | 18 | html[data-theme="daylight"] { |
20 | | - --bg:#f6f3ea; --panel:#fffdf6; --screen:#fffef9; --line:#e6e0cd; |
21 | | - --phos:#136c40; --amber:#946400; --alert:#c23a32; --ink:#292d27; --dim:#8b8a78; |
22 | | - --grid-axis:#d3cdb6; --grid-split:#efe9d7; |
23 | | - --smoke:rgba(23,114,69,.20); --lossbar:rgba(194,58,50,.30); --glow:0; |
24 | | - --smoke-hot:#0a4f2d; --smoke-mid:#2f9e63; --smoke-cool:#b08a2e; |
25 | | - --shadow:0 1px 3px rgba(80,70,40,.10), 0 1px 2px rgba(80,70,40,.06); |
26 | | - --shadow-lg:0 12px 32px rgba(80,70,40,.14); |
| 19 | + --bg:#f4f1e8; --panel:#faf8f1; --screen:#fffef9; --line:#e0dbc9; |
| 20 | + --phos:#177245; --amber:#986a00; --alert:#c23a32; --ink:#2b2f28; --dim:#8b8a7a; |
| 21 | + --grid-axis:#cfc9b4; --grid-split:#ebe6d4; |
| 22 | + --smoke:rgba(23,114,69,.20); --lossbar:rgba(194,58,50,.5); --glow:0; |
| 23 | + --smoke-hot:#0e5e37; --smoke-mid:#2f9e63; --smoke-cool:#b08a2e; |
27 | 24 | } |
| 25 | + /* Arctic: cool morning light — teal trace on frosted glass */ |
28 | 26 | html[data-theme="arctic"] { |
29 | | - --bg:#eef2f6; --panel:#ffffff; --screen:#fbfdfe; --line:#dde5ec; |
30 | | - --phos:#0b7285; --amber:#b45309; --alert:#d1433f; --ink:#22303c; --dim:#7f8f9d; |
31 | | - --grid-axis:#c9d5e0; --grid-split:#e7edf3; |
32 | | - --smoke:rgba(11,114,133,.18); --lossbar:rgba(209,67,63,.30); --glow:0; |
33 | | - --smoke-hot:#043b46; --smoke-mid:#1596ad; --smoke-cool:#b3722f; |
34 | | - --shadow:0 1px 3px rgba(30,50,70,.10), 0 1px 2px rgba(30,50,70,.06); |
35 | | - --shadow-lg:0 12px 32px rgba(30,50,70,.14); |
| 27 | + --bg:#edf1f5; --panel:#f6f9fb; --screen:#fbfdfe; --line:#d7e0e8; |
| 28 | + --phos:#0b7285; --amber:#b45309; --alert:#d1433f; --ink:#24313d; --dim:#7f8f9d; |
| 29 | + --grid-axis:#c3d0db; --grid-split:#e3ebf2; |
| 30 | + --smoke:rgba(11,114,133,.18); --lossbar:rgba(209,67,63,.5); --glow:0; |
| 31 | + --smoke-hot:#064a57; --smoke-mid:#1596ad; --smoke-cool:#b3722f; |
36 | 32 | } |
37 | 33 | * { box-sizing:border-box; margin:0; } |
38 | | - body { background:var(--bg); color:var(--ink); font-family:var(--sans); |
39 | | - padding:24px clamp(16px, 3vw, 40px); font-size:14px; line-height:1.5; |
40 | | - -webkit-font-smoothing:antialiased; } |
41 | | - header { display:flex; align-items:center; gap:16px; margin-bottom:20px; flex-wrap:wrap; } |
42 | | - .mark { color:var(--phos); font-weight:700; font-size:21px; letter-spacing:.04em; font-family:var(--mono); } |
43 | | - .tag { color:var(--dim); font-size:13px; font-weight:500; letter-spacing:.01em; } |
44 | | - .row { display:flex; gap:12px; align-items:center; margin-bottom:16px; flex-wrap:wrap; } |
| 34 | + body { background:var(--bg); color:var(--ink); font-family:var(--mono); padding:18px 22px; } |
| 35 | + header { display:flex; align-items:baseline; gap:14px; margin-bottom:14px; flex-wrap:wrap; } |
| 36 | + .mark { color:var(--phos); font-weight:700; font-size:20px; letter-spacing:.05em; } |
| 37 | + .tag { color:var(--dim); font-size:12px; } |
| 38 | + .row { display:flex; gap:12px; align-items:flex-start; margin-bottom:14px; flex-wrap:wrap; } |
45 | 39 | .pills { display:flex; gap:8px; flex-wrap:wrap; } |
46 | | - .pill { font-family:var(--sans); font-weight:600; font-size:13px; background:var(--panel); |
47 | | - color:var(--ink); border:1px solid var(--line); border-radius:999px; padding:8px 16px; |
48 | | - cursor:pointer; display:flex; align-items:center; gap:8px; box-shadow:var(--shadow); |
49 | | - transition:transform .12s ease, border-color .12s ease, color .12s ease; } |
50 | | - .pill:hover { transform:translateY(-1px); border-color:var(--dim); } |
| 40 | + .pill { font-family:var(--mono); font-size:13px; background:var(--panel); color:var(--ink); |
| 41 | + border:1px solid var(--line); border-radius:4px; padding:7px 14px; cursor:pointer; |
| 42 | + display:flex; align-items:center; gap:8px; } |
51 | 43 | .pill.on { border-color:var(--phos); color:var(--phos); } |
52 | | - .pill:focus-visible { outline:2px solid var(--phos); outline-offset:2px; } |
53 | | - .led { width:8px; height:8px; border-radius:50%; background:var(--phos); box-shadow:0 0 8px var(--phos); } |
54 | | - .led.bad { background:var(--alert); box-shadow:0 0 8px var(--alert); } |
55 | | - .rng { display:flex; align-items:center; background:var(--panel); border:1px solid var(--line); |
56 | | - border-radius:10px; overflow:hidden; box-shadow:var(--shadow); } |
57 | | - .rng span { color:var(--dim); font-size:10px; padding:0 6px 0 14px; letter-spacing:.14em; font-family:var(--mono); } |
| 44 | + .led { width:8px; height:8px; border-radius:50%; background:var(--phos); box-shadow:0 0 6px var(--phos); } |
| 45 | + .led.bad { background:var(--alert); box-shadow:0 0 6px var(--alert); } |
| 46 | + .rng { display:flex; background:var(--panel); border:1px solid var(--line); border-radius:4px; overflow:hidden; } |
| 47 | + .rng span { color:var(--dim); font-size:11px; padding:8px 10px 0 12px; letter-spacing:.1em; } |
58 | 48 | .rng button { font-family:var(--mono); font-size:12px; background:none; border:0; color:var(--dim); |
59 | | - padding:9px 13px; cursor:pointer; transition:color .12s ease, background .12s ease; } |
60 | | - .rng button:hover { color:var(--ink); } |
61 | | - .rng button.on { background:color-mix(in srgb, var(--amber) 14%, transparent); color:var(--amber); font-weight:700; } |
62 | | - .scope { background:var(--panel); border:1px solid var(--line); border-radius:16px; |
63 | | - padding:18px; box-shadow:var(--shadow-lg); } |
| 49 | + padding:8px 14px; cursor:pointer; } |
| 50 | + .rng button.on { background:#1a2018; color:var(--amber); } |
| 51 | + .scope { background:var(--panel); border:1px solid var(--line); border-radius:6px; padding:14px; } |
64 | 52 | .crt { position:relative; } |
65 | | - #chart { height:440px; background:var(--screen); border-radius:10px; } |
| 53 | + #chart { height:420px; background:var(--screen); border-radius:4px; } |
66 | 54 | html:not([data-theme]) .crt::after { |
67 | | - content:""; position:absolute; inset:0; border-radius:10px; pointer-events:none; |
| 55 | + content:""; position:absolute; inset:0; border-radius:4px; pointer-events:none; |
68 | 56 | background: |
69 | | - repeating-linear-gradient(0deg, transparent 0 3px, rgba(0,0,0,.04) 3px 4px), |
70 | | - radial-gradient(120% 90% at 50% 42%, transparent 58%, rgba(0,0,0,.34) 100%); |
| 57 | + repeating-linear-gradient(0deg, transparent 0 3px, rgba(0,0,0,.045) 3px 4px), |
| 58 | + radial-gradient(120% 90% at 50% 42%, transparent 55%, rgba(0,0,0,.38) 100%); |
71 | 59 | } |
72 | | - .readouts { display:flex; gap:0; margin-top:16px; flex-wrap:wrap; align-items:stretch; } |
73 | | - .ro { padding:2px 26px 2px 0; margin-right:26px; border-right:1px solid var(--line); } |
74 | | - .ro:last-of-type { border-right:0; } |
75 | | - .ro .v { font-size:26px; font-weight:600; color:var(--phos); font-family:var(--mono); line-height:1.15; } |
| 60 | + .readouts { display:flex; gap:34px; margin-top:12px; flex-wrap:wrap; align-items:flex-end; } |
| 61 | + .ro .v { font-size:24px; color:var(--phos); } |
76 | 62 | .ro .v.bad { color:var(--alert); } |
77 | | - .ro .l { font-size:10px; color:var(--dim); letter-spacing:.14em; margin-top:3px; font-family:var(--mono); } |
| 63 | + .ro .l { font-size:10px; color:var(--dim); letter-spacing:.12em; margin-top:2px; } |
78 | 64 | .sp { flex:1; } |
79 | | - footer { margin-top:14px; color:var(--dim); font-size:12.5px; display:flex; gap:12px; flex-wrap:wrap; } |
80 | | - #conclusion { color:var(--ink); font-weight:600; font-family:var(--mono); font-size:12.5px; } |
81 | | - .legend { color:var(--dim); font-family:var(--mono); font-size:11.5px; align-self:center; } |
| 65 | + footer { margin-top:12px; color:var(--dim); font-size:12px; display:flex; gap:10px; flex-wrap:wrap; } |
| 66 | + #conclusion { color:var(--ink); } |
| 67 | + .legend { color:var(--dim); } |
82 | 68 | </style> |
83 | 69 | </head> |
84 | 70 | <body> |
|
165 | 151 | if ((!current || !visible.some(t => t.name === current)) && visible.length) current = visible[0].name; |
166 | 152 | } |
167 | 153 |
|
168 | | -let renderSeq = 0; |
169 | 154 | async function render() { |
170 | | - const seq = ++renderSeq; |
171 | 155 | await loadTargets(); |
172 | 156 | const t = targets.find(x => x.name === current); |
173 | 157 | if (!t) return; |
174 | 158 | if (days > 0) { await renderBand(t); return; } |
175 | 159 | const rounds = await (await fetch(`/api/series?target=${encodeURIComponent(current)}&minutes=${minutes}`)).json() || []; |
176 | | - if (seq !== renderSeq) return; // 过期响应:用户已切走 |
177 | 160 |
|
178 | 161 | const smoke = [], median = [], lossBars = [], burstPts = []; |
179 | 162 | let sent = 0, recv = 0, bursts = 0; |
180 | 163 | const pool = []; |
181 | | - rounds.sort((a, b) => a.t - b.t); |
182 | | - const roundInfo = {}; |
183 | 164 | rounds.forEach(r => { |
184 | 165 | const ts = r.t * 1000; |
185 | 166 | let rmed = 0; |
|
196 | 177 | const lp = r.s ? (r.s - r.r) / r.s * 100 : 0; |
197 | 178 | if (lp > 0) lossBars.push([ts, lp]); |
198 | 179 | if (r.b) { bursts++; burstPts.push([ts, lp, r.z || 0, r.s - r.r, r.s]); } |
199 | | - roundInfo[ts] = { med: rmed, lp, lost: r.s - r.r, s: r.s, b: r.b, z: r.z || 0 }; |
200 | 180 | }); |
201 | 181 | pool.sort((a,b)=>a-b); |
202 | 182 | const q = p => pool.length ? pool[Math.min(pool.length-1, Math.floor(pool.length*p))] : 0; |
|
218 | 198 | animation: false, |
219 | 199 | grid: { left: 52, right: 46, top: 18, bottom: 42 }, |
220 | 200 | xAxis: { type: 'time', axisLine: { lineStyle: { color: cssv('--grid-axis') } }, |
221 | | - axisLabel: { color: cssv('--dim'), fontSize: 10 }, splitLine: { lineStyle: { color: cssv('--grid-split') } } }, |
| 201 | + axisLabel: { color: cssv('--dim'), fontSize: 10 }, splitLine: { lineStyle: { color: cssv('--grid-split') } }, |
| 202 | + axisPointer: { show: true, snap: false, |
| 203 | + lineStyle: { color: cssv('--dim'), opacity: .45 }, |
| 204 | + label: { show: true, backgroundColor: cssv('--panel'), color: cssv('--ink'), |
| 205 | + borderColor: cssv('--line'), borderWidth: 1, fontFamily: 'monospace', fontSize: 10, |
| 206 | + formatter: p => new Date(p.value).toLocaleTimeString() } } }, |
222 | 207 | yAxis: [ |
223 | 208 | { type: 'value', name: 'RTT ms', nameTextStyle: { color: cssv('--dim'), fontSize: 10 }, |
224 | 209 | axisLabel: { color: cssv('--dim'), fontSize: 10 }, splitLine: { lineStyle: { color: cssv('--grid-split') } } }, |
225 | 210 | { type: 'value', max: 100, show: false } |
226 | 211 | ], |
227 | | - tooltip: { |
228 | | - trigger: 'axis', |
229 | | - axisPointer: { type: 'cross', |
230 | | - lineStyle: { color: cssv('--dim'), opacity: .45 }, |
231 | | - crossStyle: { color: cssv('--dim'), opacity: .45 }, |
232 | | - label: { backgroundColor: cssv('--panel'), color: cssv('--ink'), borderColor: cssv('--line'), |
233 | | - borderWidth: 1, fontFamily: 'monospace', fontSize: 10, |
234 | | - formatter: p => p.axisDimension === 'x' |
235 | | - ? new Date(p.value).toLocaleTimeString() |
236 | | - : (+p.value).toFixed(1) } }, |
237 | | - backgroundColor: cssv('--panel'), borderColor: cssv('--line'), borderWidth: 1, |
238 | | - padding: [8, 12], extraCssText: 'border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.18)', |
239 | | - textStyle: { color: cssv('--ink'), fontSize: 12, fontFamily: 'monospace' }, |
240 | | - formatter: params => { |
241 | | - const ts = params[0] && params[0].axisValue; |
242 | | - const info = roundInfo[ts]; |
243 | | - if (!info) return ''; |
244 | | - const t = new Date(+ts).toLocaleTimeString(); |
245 | | - let html = `<b>${t}</b><br>median <b>${info.med.toFixed(1)} ms</b>`; |
246 | | - html += ` · loss <b>${info.lp.toFixed(1)}%</b> (${info.lost}/${info.s})`; |
247 | | - if (info.b) html += `<br><span style="color:${cssv('--alert')}">◆ burst · z-score ${info.z ? info.z.toFixed(2) : '—'}</span>`; |
248 | | - return html; |
249 | | - } |
250 | | - }, |
| 212 | + tooltip: { backgroundColor: cssv('--panel'), borderColor: cssv('--line'), |
| 213 | + textStyle: { color: cssv('--ink'), fontSize: 11, fontFamily: 'monospace' } }, |
251 | 214 | // 荧光束渐变:贴线的样本 = 炽亮核心,散开的样本渐冷渐暗 —— 渐变即信息 |
252 | | - visualMap: { show: false, type: 'continuous', dimension: 2, min: 0, max: 1.0, |
| 215 | + visualMap: { show: false, type: 'continuous', dimension: 2, min: 0, max: 0.5, |
253 | 216 | seriesIndex: 0, |
254 | 217 | inRange: { color: [cssv('--smoke-hot'), cssv('--smoke-mid'), cssv('--smoke-cool')], |
255 | | - opacity: [0.55, 0.25] } }, |
| 218 | + opacity: [0.5, 0.10] } }, |
256 | 219 | series: [ |
257 | | - { type: 'scatter', data: smoke, symbolSize: 2.4, silent: true, progressive: 4000 }, |
| 220 | + { type: 'scatter', data: smoke, symbolSize: 2.2, silent: true, large: true }, |
258 | 221 | { type: 'line', data: median, showSymbol: false, silent: true, z: 4, |
259 | | - lineStyle: { color: phos, width: 3, opacity: 0.08 } }, |
| 222 | + lineStyle: { color: phos, width: 7, opacity: 0.10 } }, |
260 | 223 | { type: 'line', data: median, showSymbol: false, |
261 | | - lineStyle: { color: phos, width: 1.3, shadowColor: phos, shadowBlur: +cssv('--glow') * 2 }, z: 5 }, |
| 224 | + lineStyle: { color: phos, width: 1.6, shadowColor: phos, shadowBlur: +cssv('--glow') * 2 }, z: 5 }, |
262 | 225 | { type: 'bar', yAxisIndex: 1, data: lossBars, barMaxWidth: 3, |
263 | 226 | itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, |
264 | 227 | [{ offset: 0, color: cssv('--alert') }, { offset: 1, color: cssv('--lossbar') }]) } }, |
|
289 | 252 | // P50 line, a P50–P99 band, loss bars and burst counts from server-side buckets. |
290 | 253 | async function renderBand(t) { |
291 | 254 | const rows = await (await fetch(`/api/band?target=${encodeURIComponent(current)}&days=${days}`)).json() || []; |
292 | | - if (seq !== renderSeq) return; // 过期响应:用户已切走 |
293 | 255 | const p50 = [], band = [], loss = [], bursts = []; |
294 | 256 | let lossW = 0, nSum = 0, burstSum = 0, p99max = 0, p50W = 0, p90W = 0; |
295 | 257 | rows.forEach(r => { |
|
316 | 278 | tooltip: { backgroundColor: cssv('--panel'), borderColor: cssv('--line'), |
317 | 279 | textStyle: { color: cssv('--ink'), fontSize: 11, fontFamily: 'monospace' } }, |
318 | 280 | xAxis: { type: 'time', axisLine: { lineStyle: { color: cssv('--grid-axis') } }, |
319 | | - axisLabel: { color: cssv('--dim'), fontSize: 10 }, splitLine: { lineStyle: { color: cssv('--grid-split') } } }, |
| 281 | + axisLabel: { color: cssv('--dim'), fontSize: 10 }, splitLine: { lineStyle: { color: cssv('--grid-split') } }, |
| 282 | + axisPointer: { show: true, snap: false, |
| 283 | + lineStyle: { color: cssv('--dim'), opacity: .45 }, |
| 284 | + label: { show: true, backgroundColor: cssv('--panel'), color: cssv('--ink'), |
| 285 | + borderColor: cssv('--line'), borderWidth: 1, fontFamily: 'monospace', fontSize: 10, |
| 286 | + formatter: p => new Date(p.value).toLocaleTimeString() } } }, |
320 | 287 | yAxis: [ |
321 | 288 | { type: 'value', name: 'RTT ms', nameTextStyle: { color: cssv('--dim'), fontSize: 10 }, |
322 | 289 | axisLabel: { color: cssv('--dim'), fontSize: 10 }, splitLine: { lineStyle: { color: cssv('--grid-split') } } }, |
|
0 commit comments