matterbridge 3.3.1-dev-20251008-e61b8db → 3.3.1-dev-20251009-008da25
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/CHANGELOG.md +8 -1
- package/dist/broadcastServer.js +2 -0
- package/dist/cli.js +52 -17
- package/dist/cliEmitter.js +6 -0
- package/dist/cliHistory.js +659 -0
- package/dist/frontend.js +30 -11
- package/dist/matterbridge.js +1 -0
- package/dist/matterbridgeTypes.js +2 -0
- package/frontend/build/assets/index.js +4 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
if (process.argv.includes('--loader') || process.argv.includes('-loader'))
|
|
2
|
+
console.log('\u001B[32mCli history loaded.\u001B[40;0m');
|
|
3
|
+
import { writeFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
export const historySize = 2160;
|
|
6
|
+
export let historyIndex = 0;
|
|
7
|
+
export function setHistoryIndex(index) {
|
|
8
|
+
if (!Number.isFinite(index) || !Number.isSafeInteger(index)) {
|
|
9
|
+
throw new TypeError('historyIndex must be a finite, safe integer.');
|
|
10
|
+
}
|
|
11
|
+
if (index < 0 || index >= historySize) {
|
|
12
|
+
throw new RangeError(`historyIndex must be between 0 and ${historySize - 1}.`);
|
|
13
|
+
}
|
|
14
|
+
historyIndex = index;
|
|
15
|
+
}
|
|
16
|
+
export const history = Array.from({ length: historySize }, () => ({
|
|
17
|
+
cpu: 0,
|
|
18
|
+
peakCpu: 0,
|
|
19
|
+
processCpu: 0,
|
|
20
|
+
peakProcessCpu: 0,
|
|
21
|
+
rss: 0,
|
|
22
|
+
peakRss: 0,
|
|
23
|
+
heapUsed: 0,
|
|
24
|
+
peakHeapUsed: 0,
|
|
25
|
+
heapTotal: 0,
|
|
26
|
+
peakHeapTotal: 0,
|
|
27
|
+
timestamp: 0,
|
|
28
|
+
}));
|
|
29
|
+
export function generateHistoryPage(options = {}) {
|
|
30
|
+
const pageTitle = options.pageTitle ?? 'Matterbridge CPU & Memory History';
|
|
31
|
+
const outputPath = path.resolve(options.outputPath ?? path.join(process.cwd(), 'history.html'));
|
|
32
|
+
const bufferLength = history.length;
|
|
33
|
+
if (bufferLength === 0) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const startIndex = ((Math.trunc(historyIndex) % bufferLength) + bufferLength) % bufferLength;
|
|
37
|
+
const normalizedHistory = [];
|
|
38
|
+
for (let offset = 0; offset < bufferLength; offset += 1) {
|
|
39
|
+
const index = (startIndex + offset) % bufferLength;
|
|
40
|
+
const entry = history[index];
|
|
41
|
+
if (!entry || entry.timestamp === 0)
|
|
42
|
+
continue;
|
|
43
|
+
normalizedHistory.push(entry);
|
|
44
|
+
}
|
|
45
|
+
if (normalizedHistory.length === 0) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const peakCpu = Math.max(...normalizedHistory.map((entry) => entry.peakCpu ?? entry.cpu));
|
|
49
|
+
const peakProcessCpu = Math.max(...normalizedHistory.map((entry) => entry.peakProcessCpu ?? entry.processCpu));
|
|
50
|
+
const peakRss = Math.max(...normalizedHistory.map((entry) => entry.peakRss ?? entry.rss));
|
|
51
|
+
const peakHeapUsed = Math.max(...normalizedHistory.map((entry) => entry.peakHeapUsed ?? entry.heapUsed));
|
|
52
|
+
const peakHeapTotal = Math.max(...normalizedHistory.map((entry) => entry.peakHeapTotal ?? entry.heapTotal));
|
|
53
|
+
const firstTimestamp = normalizedHistory[0]?.timestamp ?? Date.now();
|
|
54
|
+
const lastTimestamp = normalizedHistory[normalizedHistory.length - 1]?.timestamp ?? Date.now();
|
|
55
|
+
const historySanitised = JSON.stringify(normalizedHistory).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
|
|
56
|
+
const summary = {
|
|
57
|
+
entries: normalizedHistory.length,
|
|
58
|
+
timeRange: `${new Date(firstTimestamp).toLocaleString()} → ${new Date(lastTimestamp).toLocaleString()}`,
|
|
59
|
+
peakCpu,
|
|
60
|
+
peakProcessCpu,
|
|
61
|
+
peakRss,
|
|
62
|
+
peakHeapUsed,
|
|
63
|
+
peakHeapTotal,
|
|
64
|
+
};
|
|
65
|
+
const summarySanitised = JSON.stringify(summary).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
|
|
66
|
+
const html = `<!DOCTYPE html>
|
|
67
|
+
<html lang="en">
|
|
68
|
+
<head>
|
|
69
|
+
<meta charset="UTF-8" />
|
|
70
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
71
|
+
<title>${escapeHtml(pageTitle)}</title>
|
|
72
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
73
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
74
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
75
|
+
<style>
|
|
76
|
+
:root {
|
|
77
|
+
color-scheme: dark light;
|
|
78
|
+
--bg: #0f172a;
|
|
79
|
+
--bg-card: rgba(15, 23, 42, 0.72);
|
|
80
|
+
--fg: #e2e8f0;
|
|
81
|
+
--accent: #38bdf8;
|
|
82
|
+
--muted: #94a3b8;
|
|
83
|
+
--border: rgba(148, 163, 184, 0.2);
|
|
84
|
+
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
85
|
+
}
|
|
86
|
+
body {
|
|
87
|
+
margin: 0;
|
|
88
|
+
padding: 20px;
|
|
89
|
+
background: linear-gradient(145deg, #020617, #0f172a);
|
|
90
|
+
color: var(--fg);
|
|
91
|
+
min-width: 320px;
|
|
92
|
+
min-height: 100vh;
|
|
93
|
+
}
|
|
94
|
+
h1 {
|
|
95
|
+
margin-top: 0;
|
|
96
|
+
font-size: clamp(1.6rem, 2.2vw, 2.3rem);
|
|
97
|
+
font-weight: 700;
|
|
98
|
+
color: var(--accent);
|
|
99
|
+
}
|
|
100
|
+
.container {
|
|
101
|
+
min-width: 320px;
|
|
102
|
+
max-width: 1240px;
|
|
103
|
+
margin: 0 auto;
|
|
104
|
+
padding: 0;
|
|
105
|
+
display: grid;
|
|
106
|
+
gap: 20px;
|
|
107
|
+
}
|
|
108
|
+
.card {
|
|
109
|
+
background: var(--bg-card);
|
|
110
|
+
border: 1px solid var(--border);
|
|
111
|
+
border-radius: 16px;
|
|
112
|
+
padding: 20px;
|
|
113
|
+
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.35);
|
|
114
|
+
backdrop-filter: blur(12px);
|
|
115
|
+
margin: 0;
|
|
116
|
+
width: calc(100% - 40px);
|
|
117
|
+
max-width: 1200px;
|
|
118
|
+
position: relative;
|
|
119
|
+
overflow: hidden;
|
|
120
|
+
}
|
|
121
|
+
.summary-grid {
|
|
122
|
+
display: grid;
|
|
123
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
124
|
+
gap: 16px;
|
|
125
|
+
}
|
|
126
|
+
.summary-item {
|
|
127
|
+
border: 1px solid var(--border);
|
|
128
|
+
border-radius: 12px;
|
|
129
|
+
padding: 16px;
|
|
130
|
+
background: rgba(30, 41, 59, 0.85);
|
|
131
|
+
}
|
|
132
|
+
.summary-item h2 {
|
|
133
|
+
margin: 0 0 8px;
|
|
134
|
+
font-size: 0.95rem;
|
|
135
|
+
text-transform: uppercase;
|
|
136
|
+
letter-spacing: 0.08em;
|
|
137
|
+
color: var(--muted);
|
|
138
|
+
}
|
|
139
|
+
.summary-item p {
|
|
140
|
+
margin: 0;
|
|
141
|
+
font-size: 1.25rem;
|
|
142
|
+
font-weight: 600;
|
|
143
|
+
}
|
|
144
|
+
canvas {
|
|
145
|
+
width: min(100%, 1200px);
|
|
146
|
+
height: 320px;
|
|
147
|
+
display: block;
|
|
148
|
+
margin: 0 auto;
|
|
149
|
+
}
|
|
150
|
+
.chart-legend {
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-wrap: wrap;
|
|
153
|
+
gap: 12px;
|
|
154
|
+
margin-top: 16px;
|
|
155
|
+
font-size: 0.85rem;
|
|
156
|
+
color: var(--muted);
|
|
157
|
+
}
|
|
158
|
+
.chart-legend span {
|
|
159
|
+
display: inline-flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
gap: 8px;
|
|
162
|
+
}
|
|
163
|
+
.chart-legend span::before {
|
|
164
|
+
content: '';
|
|
165
|
+
width: 12px;
|
|
166
|
+
height: 12px;
|
|
167
|
+
border-radius: 999px;
|
|
168
|
+
background: currentColor;
|
|
169
|
+
opacity: 0.85;
|
|
170
|
+
border: 1px solid rgba(255, 255, 255, 0.4);
|
|
171
|
+
}
|
|
172
|
+
table {
|
|
173
|
+
width: 100%;
|
|
174
|
+
border-collapse: collapse;
|
|
175
|
+
font-size: 0.95rem;
|
|
176
|
+
}
|
|
177
|
+
th, td {
|
|
178
|
+
padding: 8px;
|
|
179
|
+
border-bottom: 1px solid var(--border);
|
|
180
|
+
text-align: left;
|
|
181
|
+
white-space: nowrap;
|
|
182
|
+
}
|
|
183
|
+
th {
|
|
184
|
+
text-transform: uppercase;
|
|
185
|
+
font-size: 0.75rem;
|
|
186
|
+
letter-spacing: 0.08em;
|
|
187
|
+
color: var(--muted);
|
|
188
|
+
}
|
|
189
|
+
td {
|
|
190
|
+
font-size: 0.75rem;
|
|
191
|
+
}
|
|
192
|
+
tr:hover {
|
|
193
|
+
background: rgba(148, 163, 184, 0.08);
|
|
194
|
+
}
|
|
195
|
+
.table-wrapper {
|
|
196
|
+
overflow: auto;
|
|
197
|
+
max-height: 400px;
|
|
198
|
+
scrollbar-color: rgba(148, 163, 184, 0.35) var(--bg-card);
|
|
199
|
+
scrollbar-width: thin;
|
|
200
|
+
}
|
|
201
|
+
.table-wrapper::-webkit-scrollbar {
|
|
202
|
+
width: 10px;
|
|
203
|
+
}
|
|
204
|
+
.table-wrapper::-webkit-scrollbar-track {
|
|
205
|
+
background: var(--bg-card);
|
|
206
|
+
}
|
|
207
|
+
.table-wrapper::-webkit-scrollbar-thumb {
|
|
208
|
+
background-color: rgba(148, 163, 184, 0.45);
|
|
209
|
+
border-radius: 999px;
|
|
210
|
+
border: 2px solid var(--bg-card);
|
|
211
|
+
}
|
|
212
|
+
@media (max-width: 720px) {
|
|
213
|
+
body {
|
|
214
|
+
padding: 16px;
|
|
215
|
+
}
|
|
216
|
+
.card {
|
|
217
|
+
padding: 18px;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
</style>
|
|
221
|
+
</head>
|
|
222
|
+
<body>
|
|
223
|
+
<div class="container">
|
|
224
|
+
<header>
|
|
225
|
+
<h1>${escapeHtml(pageTitle)}</h1>
|
|
226
|
+
<p>Generated ${new Date().toLocaleString()}</p>
|
|
227
|
+
</header>
|
|
228
|
+
|
|
229
|
+
<section class="card">
|
|
230
|
+
<div class="summary-grid" id="summary"></div>
|
|
231
|
+
</section>
|
|
232
|
+
|
|
233
|
+
<section class="card">
|
|
234
|
+
<h2>Host CPU Usage (%)</h2>
|
|
235
|
+
<canvas id="cpuChart"></canvas>
|
|
236
|
+
</section>
|
|
237
|
+
|
|
238
|
+
<section class="card">
|
|
239
|
+
<h2>Process CPU Usage (%)</h2>
|
|
240
|
+
<canvas id="processCpuChart"></canvas>
|
|
241
|
+
</section>
|
|
242
|
+
|
|
243
|
+
<section class="card">
|
|
244
|
+
<h2>Memory Usage (MB)</h2>
|
|
245
|
+
<canvas id="memoryChart"></canvas>
|
|
246
|
+
</section>
|
|
247
|
+
|
|
248
|
+
<section class="card">
|
|
249
|
+
<h2>Samples</h2>
|
|
250
|
+
<div class="table-wrapper">
|
|
251
|
+
<table>
|
|
252
|
+
<thead>
|
|
253
|
+
<tr>
|
|
254
|
+
<th>Timestamp</th>
|
|
255
|
+
<th>Host CPU %</th>
|
|
256
|
+
<th title="Host CPU Peak">Peak %</th>
|
|
257
|
+
<th>Process CPU %</th>
|
|
258
|
+
<th title="Process CPU Peak">Peak %</th>
|
|
259
|
+
<th>RSS (MB)</th>
|
|
260
|
+
<th title="RSS Peak">Peak MB</th>
|
|
261
|
+
<th>Heap Used (MB)</th>
|
|
262
|
+
<th title="Heap Used Peak">Peak MB</th>
|
|
263
|
+
<th>Heap Total (MB)</th>
|
|
264
|
+
<th title="Heap Total Peak">Peak MB</th>
|
|
265
|
+
</tr>
|
|
266
|
+
</thead>
|
|
267
|
+
<tbody id="historyTable"></tbody>
|
|
268
|
+
</table>
|
|
269
|
+
</div>
|
|
270
|
+
</section>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<script type="module">
|
|
274
|
+
const HISTORY_DATA = ${historySanitised};
|
|
275
|
+
const SUMMARY_DATA = ${summarySanitised};
|
|
276
|
+
let cleanup = () => {};
|
|
277
|
+
|
|
278
|
+
const summaryContainer = document.getElementById('summary');
|
|
279
|
+
const summaryEntries = [
|
|
280
|
+
{ label: 'Samples', value: SUMMARY_DATA.entries.toLocaleString() },
|
|
281
|
+
{ label: 'Time Range', value: SUMMARY_DATA.timeRange },
|
|
282
|
+
{ label: 'Host CPU Peak', value: SUMMARY_DATA.peakCpu.toFixed(2) + ' %' },
|
|
283
|
+
{ label: 'Process CPU Peak', value: SUMMARY_DATA.peakProcessCpu.toFixed(2) + ' %' },
|
|
284
|
+
{ label: 'RSS Peak', value: formatBytes(SUMMARY_DATA.peakRss) },
|
|
285
|
+
{ label: 'Heap Used Peak', value: formatBytes(SUMMARY_DATA.peakHeapUsed) },
|
|
286
|
+
{ label: 'Heap Total Peak', value: formatBytes(SUMMARY_DATA.peakHeapTotal) }
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
summaryEntries.forEach(function (itemData) {
|
|
290
|
+
const item = document.createElement('div');
|
|
291
|
+
item.className = 'summary-item';
|
|
292
|
+
item.innerHTML = '<h2>' + itemData.label + '</h2><p>' + itemData.value + '</p>';
|
|
293
|
+
summaryContainer.appendChild(item);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const tableBody = document.getElementById('historyTable');
|
|
297
|
+
HISTORY_DATA.forEach(function (entry) {
|
|
298
|
+
const row = document.createElement('tr');
|
|
299
|
+
const cells = [
|
|
300
|
+
new Date(entry.timestamp).toLocaleString(),
|
|
301
|
+
entry.cpu.toFixed(2),
|
|
302
|
+
entry.peakCpu.toFixed(2),
|
|
303
|
+
(Number.isFinite(entry.processCpu) ? entry.processCpu : 0).toFixed(2),
|
|
304
|
+
(
|
|
305
|
+
Number.isFinite(entry.peakProcessCpu)
|
|
306
|
+
? entry.peakProcessCpu
|
|
307
|
+
: Number.isFinite(entry.processCpu)
|
|
308
|
+
? entry.processCpu
|
|
309
|
+
: 0
|
|
310
|
+
).toFixed(2),
|
|
311
|
+
bytesToMb(entry.rss).toFixed(2),
|
|
312
|
+
bytesToMb(entry.peakRss).toFixed(2),
|
|
313
|
+
bytesToMb(entry.heapUsed).toFixed(2),
|
|
314
|
+
bytesToMb(entry.peakHeapUsed).toFixed(2),
|
|
315
|
+
bytesToMb(entry.heapTotal).toFixed(2),
|
|
316
|
+
bytesToMb(entry.peakHeapTotal).toFixed(2)
|
|
317
|
+
];
|
|
318
|
+
row.innerHTML = '<td>' + cells.join('</td><td>') + '</td>';
|
|
319
|
+
tableBody.appendChild(row);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const labels = HISTORY_DATA.map(function (entry) {
|
|
323
|
+
return formatTimestamp(entry.timestamp);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const cpuPeakValue = HISTORY_DATA.reduce(function (acc, entry) {
|
|
327
|
+
return Math.max(acc, Number.isFinite(entry.peakCpu) ? entry.peakCpu : 0, Number.isFinite(entry.cpu) ? entry.cpu : 0);
|
|
328
|
+
}, 0);
|
|
329
|
+
const cpuPadding = cpuPeakValue <= 10 ? 2 : cpuPeakValue * 0.1;
|
|
330
|
+
|
|
331
|
+
const processCpuPeakValue = HISTORY_DATA.reduce(function (acc, entry) {
|
|
332
|
+
return Math.max(
|
|
333
|
+
acc,
|
|
334
|
+
Number.isFinite(entry.peakProcessCpu) ? entry.peakProcessCpu : 0,
|
|
335
|
+
Number.isFinite(entry.processCpu) ? entry.processCpu : 0
|
|
336
|
+
);
|
|
337
|
+
}, 0);
|
|
338
|
+
const processCpuPadding = processCpuPeakValue <= 10 ? 2 : processCpuPeakValue * 0.1;
|
|
339
|
+
|
|
340
|
+
renderCharts();
|
|
341
|
+
|
|
342
|
+
function renderCharts() {
|
|
343
|
+
cleanup();
|
|
344
|
+
|
|
345
|
+
function draw() {
|
|
346
|
+
renderLineChart('cpuChart', {
|
|
347
|
+
labels: labels,
|
|
348
|
+
datasets: [
|
|
349
|
+
{
|
|
350
|
+
label: 'Host CPU %',
|
|
351
|
+
values: HISTORY_DATA.map(function (entry) {
|
|
352
|
+
return Number.isFinite(entry.cpu) ? Number(entry.cpu.toFixed(2)) : 0;
|
|
353
|
+
}),
|
|
354
|
+
color: '#38bdf8',
|
|
355
|
+
fill: 'rgba(56, 189, 248, 0.18)'
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
label: 'Host Peak CPU %',
|
|
359
|
+
values: HISTORY_DATA.map(function (entry) {
|
|
360
|
+
return Number.isFinite(entry.peakCpu) ? Number(entry.peakCpu.toFixed(2)) : 0;
|
|
361
|
+
}),
|
|
362
|
+
color: '#facc15',
|
|
363
|
+
dashed: [6, 4]
|
|
364
|
+
}
|
|
365
|
+
],
|
|
366
|
+
minY: 0,
|
|
367
|
+
maxY: cpuPeakValue + cpuPadding,
|
|
368
|
+
yFormatter: function (value) {
|
|
369
|
+
return value.toFixed(0) + ' %';
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
renderLineChart('processCpuChart', {
|
|
374
|
+
labels: labels,
|
|
375
|
+
datasets: [
|
|
376
|
+
{
|
|
377
|
+
label: 'Process CPU %',
|
|
378
|
+
values: HISTORY_DATA.map(function (entry) {
|
|
379
|
+
return Number.isFinite(entry.processCpu) ? Number(entry.processCpu.toFixed(2)) : 0;
|
|
380
|
+
}),
|
|
381
|
+
color: '#a855f7',
|
|
382
|
+
fill: 'rgba(168, 85, 247, 0.18)'
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
label: 'Process Peak CPU %',
|
|
386
|
+
values: HISTORY_DATA.map(function (entry) {
|
|
387
|
+
if (Number.isFinite(entry.peakProcessCpu)) {
|
|
388
|
+
return Number(entry.peakProcessCpu.toFixed(2));
|
|
389
|
+
}
|
|
390
|
+
if (Number.isFinite(entry.processCpu)) {
|
|
391
|
+
return Number(entry.processCpu.toFixed(2));
|
|
392
|
+
}
|
|
393
|
+
return 0;
|
|
394
|
+
}),
|
|
395
|
+
color: '#f97316',
|
|
396
|
+
dashed: [6, 4]
|
|
397
|
+
}
|
|
398
|
+
],
|
|
399
|
+
minY: 0,
|
|
400
|
+
maxY: processCpuPeakValue + processCpuPadding,
|
|
401
|
+
yFormatter: function (value) {
|
|
402
|
+
return value.toFixed(0) + ' %';
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
renderLineChart('memoryChart', {
|
|
407
|
+
labels: labels,
|
|
408
|
+
datasets: [
|
|
409
|
+
{
|
|
410
|
+
label: 'RSS (MB)',
|
|
411
|
+
values: HISTORY_DATA.map(function (entry) { return bytesToMb(entry.rss); }),
|
|
412
|
+
color: '#34d399',
|
|
413
|
+
fill: 'rgba(52, 211, 153, 0.18)'
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
label: 'Heap Total (MB)',
|
|
417
|
+
values: HISTORY_DATA.map(function (entry) { return bytesToMb(entry.heapTotal); }),
|
|
418
|
+
color: '#fb923c'
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
label: 'Heap Used (MB)',
|
|
422
|
+
values: HISTORY_DATA.map(function (entry) { return bytesToMb(entry.heapUsed); }),
|
|
423
|
+
color: '#f472b6'
|
|
424
|
+
}
|
|
425
|
+
],
|
|
426
|
+
minY: 0,
|
|
427
|
+
yFormatter: function (value) {
|
|
428
|
+
return value.toFixed(0) + ' MB';
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
draw();
|
|
434
|
+
|
|
435
|
+
const debouncedRender = debounce(draw, 150);
|
|
436
|
+
window.addEventListener('resize', debouncedRender);
|
|
437
|
+
|
|
438
|
+
cleanup = function () {
|
|
439
|
+
window.removeEventListener('resize', debouncedRender);
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function renderLineChart(canvasId, config) {
|
|
444
|
+
const canvas = document.getElementById(canvasId);
|
|
445
|
+
if (!canvas) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const parent = canvas.parentElement;
|
|
450
|
+
if (parent) {
|
|
451
|
+
const existingLegend = parent.querySelector('.chart-legend');
|
|
452
|
+
if (existingLegend) {
|
|
453
|
+
existingLegend.remove();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const parentWidth = parent && parent.clientWidth ? parent.clientWidth : canvas.clientWidth || 720;
|
|
458
|
+
const cssHeight = canvas.dataset.height ? Number(canvas.dataset.height) : 320;
|
|
459
|
+
const dpr = window.devicePixelRatio || 1;
|
|
460
|
+
|
|
461
|
+
canvas.width = parentWidth * dpr;
|
|
462
|
+
canvas.height = cssHeight * dpr;
|
|
463
|
+
canvas.style.width = '100%';
|
|
464
|
+
canvas.style.height = cssHeight + 'px';
|
|
465
|
+
|
|
466
|
+
const ctx = canvas.getContext('2d');
|
|
467
|
+
if (!ctx) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
472
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
473
|
+
ctx.scale(dpr, dpr);
|
|
474
|
+
|
|
475
|
+
const margin = { top: 20, right: 24, bottom: 48, left: 72 };
|
|
476
|
+
const innerWidth = Math.max(10, parentWidth - margin.left - margin.right);
|
|
477
|
+
const innerHeight = Math.max(10, cssHeight - margin.top - margin.bottom);
|
|
478
|
+
|
|
479
|
+
ctx.translate(margin.left, margin.top);
|
|
480
|
+
|
|
481
|
+
const allValues = [];
|
|
482
|
+
config.datasets.forEach(function (dataset) {
|
|
483
|
+
dataset.values.forEach(function (value) {
|
|
484
|
+
if (Number.isFinite(value)) {
|
|
485
|
+
allValues.push(value);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
let minY = typeof config.minY === 'number' ? config.minY : Math.min.apply(Math, allValues);
|
|
491
|
+
let maxY = typeof config.maxY === 'number' ? config.maxY : Math.max.apply(Math, allValues);
|
|
492
|
+
|
|
493
|
+
if (!Number.isFinite(minY) || !Number.isFinite(maxY)) {
|
|
494
|
+
minY = 0;
|
|
495
|
+
maxY = 1;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (minY === maxY) {
|
|
499
|
+
const padding = Math.abs(minY) * 0.05 || 1;
|
|
500
|
+
minY -= padding;
|
|
501
|
+
maxY += padding;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const valueRange = maxY - minY;
|
|
505
|
+
const gridLines = config.yTicks || 4;
|
|
506
|
+
|
|
507
|
+
ctx.lineWidth = 1;
|
|
508
|
+
ctx.font = '12px Inter, sans-serif';
|
|
509
|
+
ctx.textAlign = 'right';
|
|
510
|
+
ctx.textBaseline = 'middle';
|
|
511
|
+
ctx.fillStyle = '#94a3b8';
|
|
512
|
+
|
|
513
|
+
for (let i = 0; i <= gridLines; i += 1) {
|
|
514
|
+
const ratio = i / gridLines;
|
|
515
|
+
const y = innerHeight - ratio * innerHeight;
|
|
516
|
+
ctx.strokeStyle = 'rgba(148, 163, 184, 0.12)';
|
|
517
|
+
ctx.beginPath();
|
|
518
|
+
ctx.moveTo(0, y);
|
|
519
|
+
ctx.lineTo(innerWidth, y);
|
|
520
|
+
ctx.stroke();
|
|
521
|
+
|
|
522
|
+
const value = minY + ratio * valueRange;
|
|
523
|
+
const label = config.yFormatter ? config.yFormatter(value) : value.toFixed(1);
|
|
524
|
+
ctx.fillText(label, -12, y);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const xCount = config.labels.length;
|
|
528
|
+
const xTicks = config.xTickCount || Math.min(6, xCount);
|
|
529
|
+
const xStep = xCount > 1 ? Math.max(1, Math.floor(xCount / xTicks)) : 1;
|
|
530
|
+
|
|
531
|
+
ctx.textAlign = 'center';
|
|
532
|
+
ctx.textBaseline = 'top';
|
|
533
|
+
|
|
534
|
+
for (let i = 0; i < xCount; i += xStep) {
|
|
535
|
+
const x = xCount === 1 ? innerWidth / 2 : (i / (xCount - 1)) * innerWidth;
|
|
536
|
+
ctx.strokeStyle = 'rgba(148, 163, 184, 0.08)';
|
|
537
|
+
ctx.beginPath();
|
|
538
|
+
ctx.moveTo(x, 0);
|
|
539
|
+
ctx.lineTo(x, innerHeight);
|
|
540
|
+
ctx.stroke();
|
|
541
|
+
|
|
542
|
+
ctx.save();
|
|
543
|
+
ctx.translate(x, innerHeight + 14);
|
|
544
|
+
ctx.rotate(-Math.PI / 8);
|
|
545
|
+
ctx.fillStyle = '#94a3b8';
|
|
546
|
+
ctx.fillText(config.labels[i], 0, 0);
|
|
547
|
+
ctx.restore();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
ctx.strokeStyle = 'rgba(148, 163, 184, 0.35)';
|
|
551
|
+
ctx.lineWidth = 1.2;
|
|
552
|
+
ctx.beginPath();
|
|
553
|
+
ctx.moveTo(0, 0);
|
|
554
|
+
ctx.lineTo(0, innerHeight);
|
|
555
|
+
ctx.lineTo(innerWidth, innerHeight);
|
|
556
|
+
ctx.stroke();
|
|
557
|
+
|
|
558
|
+
config.datasets.forEach(function (dataset) {
|
|
559
|
+
const points = dataset.values.map(function (value, index) {
|
|
560
|
+
const safeValue = Number.isFinite(value) ? value : minY;
|
|
561
|
+
const x = xCount === 1 ? innerWidth / 2 : (index / (xCount - 1)) * innerWidth;
|
|
562
|
+
const ratio = (safeValue - minY) / valueRange;
|
|
563
|
+
const y = innerHeight - ratio * innerHeight;
|
|
564
|
+
return { x: x, y: y };
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
if (dataset.fill && points.length > 1) {
|
|
568
|
+
ctx.fillStyle = dataset.fill;
|
|
569
|
+
ctx.beginPath();
|
|
570
|
+
points.forEach(function (point, pointIndex) {
|
|
571
|
+
if (pointIndex === 0) {
|
|
572
|
+
ctx.moveTo(point.x, point.y);
|
|
573
|
+
} else {
|
|
574
|
+
ctx.lineTo(point.x, point.y);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
const last = points[points.length - 1];
|
|
578
|
+
const first = points[0];
|
|
579
|
+
ctx.lineTo(last.x, innerHeight);
|
|
580
|
+
ctx.lineTo(first.x, innerHeight);
|
|
581
|
+
ctx.closePath();
|
|
582
|
+
ctx.fill();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (dataset.dashed) {
|
|
586
|
+
ctx.setLineDash(dataset.dashed);
|
|
587
|
+
} else {
|
|
588
|
+
ctx.setLineDash([]);
|
|
589
|
+
}
|
|
590
|
+
ctx.strokeStyle = dataset.color;
|
|
591
|
+
ctx.lineWidth = dataset.width || 2;
|
|
592
|
+
ctx.beginPath();
|
|
593
|
+
points.forEach(function (point, pointIndex) {
|
|
594
|
+
if (pointIndex === 0) {
|
|
595
|
+
ctx.moveTo(point.x, point.y);
|
|
596
|
+
} else {
|
|
597
|
+
ctx.lineTo(point.x, point.y);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
ctx.stroke();
|
|
601
|
+
ctx.setLineDash([]);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
605
|
+
|
|
606
|
+
if (parent) {
|
|
607
|
+
const legend = document.createElement('div');
|
|
608
|
+
legend.className = 'chart-legend';
|
|
609
|
+
config.datasets.forEach(function (dataset) {
|
|
610
|
+
const legendItem = document.createElement('span');
|
|
611
|
+
legendItem.style.color = dataset.color;
|
|
612
|
+
legendItem.textContent = dataset.label;
|
|
613
|
+
legend.appendChild(legendItem);
|
|
614
|
+
});
|
|
615
|
+
parent.appendChild(legend);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function formatTimestamp(timestamp) {
|
|
620
|
+
const date = new Date(timestamp);
|
|
621
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function debounce(fn, delay) {
|
|
625
|
+
let timeout;
|
|
626
|
+
return function () {
|
|
627
|
+
const context = this;
|
|
628
|
+
const args = arguments;
|
|
629
|
+
clearTimeout(timeout);
|
|
630
|
+
timeout = setTimeout(function () {
|
|
631
|
+
fn.apply(context, args);
|
|
632
|
+
}, delay);
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function bytesToMb(bytes) {
|
|
637
|
+
return bytes / (1024 * 1024);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function formatBytes(bytes) {
|
|
641
|
+
if (!Number.isFinite(bytes)) return '0 B';
|
|
642
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
643
|
+
let value = bytes;
|
|
644
|
+
let unitIndex = 0;
|
|
645
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
646
|
+
value /= 1024;
|
|
647
|
+
unitIndex += 1;
|
|
648
|
+
}
|
|
649
|
+
return value.toFixed(2) + ' ' + units[unitIndex];
|
|
650
|
+
}
|
|
651
|
+
</script>
|
|
652
|
+
</body>
|
|
653
|
+
</html>`;
|
|
654
|
+
writeFileSync(outputPath, html, { encoding: 'utf-8' });
|
|
655
|
+
return outputPath;
|
|
656
|
+
}
|
|
657
|
+
function escapeHtml(input) {
|
|
658
|
+
return input.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
659
|
+
}
|