@profullstack/hqtui-demo 0.1.2 → 0.1.3

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.
Files changed (44) hide show
  1. package/README.md +51 -8
  2. package/dist/main.js +34 -11
  3. package/dist/main.js.map +1 -1
  4. package/dist/screens/index.js +4 -0
  5. package/dist/screens/index.js.map +1 -1
  6. package/dist/screens/network.js +86 -0
  7. package/dist/screens/network.js.map +1 -0
  8. package/dist/screens/services.js +121 -0
  9. package/dist/screens/services.js.map +1 -0
  10. package/dist/screens/sessions.js +80 -0
  11. package/dist/screens/sessions.js.map +1 -0
  12. package/dist/screens/traffic.js +183 -0
  13. package/dist/screens/traffic.js.map +1 -0
  14. package/dist/simulation.js +6 -0
  15. package/dist/simulation.js.map +1 -1
  16. package/dist/state.js +4 -1
  17. package/dist/state.js.map +1 -1
  18. package/dist/system/common.js +2 -0
  19. package/dist/system/common.js.map +1 -1
  20. package/dist/system/linux-telemetry.js +420 -0
  21. package/dist/system/linux-telemetry.js.map +1 -0
  22. package/dist/system/linux-traffic.js +282 -0
  23. package/dist/system/linux-traffic.js.map +1 -0
  24. package/dist/system/linux.js +90 -26
  25. package/dist/system/linux.js.map +1 -1
  26. package/dist/system/simulated-telemetry.js +267 -0
  27. package/dist/system/simulated-telemetry.js.map +1 -0
  28. package/dist/system/telemetry.js +44 -0
  29. package/dist/system/telemetry.js.map +1 -0
  30. package/package.json +4 -3
  31. package/src/main.ts +26 -11
  32. package/src/screens/index.ts +4 -0
  33. package/src/screens/network.ts +91 -0
  34. package/src/screens/services.ts +136 -0
  35. package/src/screens/sessions.ts +85 -0
  36. package/src/screens/traffic.ts +199 -0
  37. package/src/simulation.ts +12 -0
  38. package/src/state.ts +7 -2
  39. package/src/system/common.ts +2 -0
  40. package/src/system/linux-telemetry.ts +431 -0
  41. package/src/system/linux-traffic.ts +375 -0
  42. package/src/system/linux.ts +95 -21
  43. package/src/system/simulated-telemetry.ts +280 -0
  44. package/src/system/telemetry.ts +263 -0
@@ -0,0 +1,136 @@
1
+ import type { Container, Theme } from "@profullstack/hqtui";
2
+ import type { DemoState } from "../state.ts";
3
+ import { bytes, num, percent } from "../format.ts";
4
+
5
+ function rate(value: number): string {
6
+ if (value >= 1000) return `${(value / 1000).toFixed(1)}K/s`;
7
+ return `${value.toFixed(0)}/s`;
8
+ }
9
+
10
+ /** Services, containers, kernel counters, filesystems and hardware sensors. */
11
+ export function servicesScreen(ui: Container, state: DemoState, theme: Theme): void {
12
+ const t = state.sample.telemetry;
13
+ const failed = t.services.filter((s) => s.active === "failed");
14
+
15
+ ui.row({ size: "1fr", gap: 1 }, (row) => {
16
+ row.panel({
17
+ title: "Services",
18
+ subtitle: failed.length ? `${failed.length} failed` : `${t.services.length} units`,
19
+ subtitleColor: failed.length ? theme.danger : theme.muted,
20
+ borderColor: failed.length ? theme.danger : theme.success,
21
+ }, (p) => {
22
+ if (t.services.length === 0) {
23
+ p.label("systemd not available on this host.");
24
+ return;
25
+ }
26
+ p.table({
27
+ rows: t.services,
28
+ selected: state.selected % Math.max(1, t.services.length),
29
+ offset: state.offset,
30
+ zebra: true,
31
+ scrollbar: true,
32
+ columns: [
33
+ { key: "name", title: "Unit", min: 18, color: theme.primary },
34
+ {
35
+ key: "active",
36
+ title: "Active",
37
+ width: 10,
38
+ color: (row) =>
39
+ row.active === "failed" ? theme.danger : row.active === "active" ? theme.success : theme.muted,
40
+ },
41
+ { key: "sub", title: "Sub", width: 10, color: theme.muted },
42
+ { key: "description", title: "Description", min: 16, color: theme.muted },
43
+ ],
44
+ });
45
+ });
46
+
47
+ row.column({ width: "0.85fr", gap: 1 }, (column) => {
48
+ column.panel({ title: "Kernel", size: 11, borderColor: theme.accent }, (p) => {
49
+ const k = t.kernel;
50
+ p.keyValues([
51
+ { label: "Context switches", value: rate(k.contextSwitchRate), color: theme.accent },
52
+ { label: "Interrupts", value: rate(k.interruptRate), color: theme.accent },
53
+ { label: "Forks", value: rate(k.forkRate), color: theme.accent },
54
+ { label: "Procs running", value: String(k.procsRunning), color: theme.success },
55
+ { label: "Procs blocked", value: String(k.procsBlocked), color: k.procsBlocked ? theme.warning : theme.muted },
56
+ { label: "Open file descriptors", value: k.openFiles.toLocaleString(), color: theme.primary },
57
+ { label: "Entropy available", value: String(k.entropy), color: k.entropy < 200 ? theme.warning : theme.success },
58
+ { label: "Page in / out", value: `${(k.pageIn / 1000).toFixed(0)}K / ${(k.pageOut / 1000).toFixed(0)}K`, color: theme.muted },
59
+ ]);
60
+ });
61
+
62
+ column.panel({ title: "Containers", size: 9, borderColor: theme.primary }, (p) => {
63
+ if (t.containers.length === 0) {
64
+ p.label("No running containers.");
65
+ p.label("(docker not installed or not reachable)", { size: 1 });
66
+ return;
67
+ }
68
+ p.table({
69
+ rows: t.containers,
70
+ zebra: true,
71
+ columns: [
72
+ { key: "name", title: "Name", min: 12, color: theme.primary },
73
+ { key: "image", title: "Image", min: 14, color: theme.muted },
74
+ { key: "status", title: "Status", min: 12, color: theme.success },
75
+ ],
76
+ });
77
+ });
78
+
79
+ column.panel({ title: "Hardware", size: "1fr", borderColor: theme.warning }, (p) => {
80
+ const rows: { label: string; value: string; color?: number }[] = [];
81
+ if (t.power) {
82
+ rows.push(
83
+ { label: "Battery", value: `${t.power.battery}% (${t.power.timeRemaining})`, color: theme.success },
84
+ { label: "AC", value: t.power.acConnected ? "connected" : "on battery", color: theme.muted },
85
+ { label: "Draw", value: `${num(t.power.powerDraw, 1)} W`, color: theme.warning },
86
+ );
87
+ }
88
+ for (const gpu of t.gpus) {
89
+ rows.push(
90
+ { label: gpu.name, value: `${percent(gpu.utilization)} · ${gpu.temperature}°C`, color: theme.accent },
91
+ { label: "GPU memory", value: `${bytes(gpu.memoryUsed)} / ${bytes(gpu.memoryTotal)}`, color: theme.muted },
92
+ );
93
+ }
94
+ if (rows.length === 0) {
95
+ p.label("No battery or GPU telemetry on this host.");
96
+ return;
97
+ }
98
+ p.keyValues(rows);
99
+ });
100
+ });
101
+ });
102
+
103
+ ui.panel({ title: "Filesystems", size: 10, borderColor: theme.secondary }, (p) => {
104
+ if (t.filesystems.length === 0) {
105
+ p.label("No filesystems reported.");
106
+ return;
107
+ }
108
+ p.table({
109
+ rows: t.filesystems,
110
+ zebra: true,
111
+ columns: [
112
+ { key: "mount", title: "Mount", min: 14, color: theme.primary },
113
+ { key: "device", title: "Device", min: 12, color: theme.muted },
114
+ { key: "type", title: "Type", width: 8, color: theme.muted },
115
+ { key: "size", title: "Size", width: 10, align: "right", render: (r) => bytes(r.size, 0) },
116
+ { key: "used", title: "Used", width: 10, align: "right", render: (r) => bytes(r.used, 0) },
117
+ {
118
+ key: "pct",
119
+ title: "Use%",
120
+ width: 6,
121
+ align: "right",
122
+ render: (r) => (r.size ? percent(r.used / r.size) : "-"),
123
+ color: (r) => (r.size && r.used / r.size > 0.9 ? theme.danger : theme.warning),
124
+ },
125
+ {
126
+ key: "inodes",
127
+ title: "Inodes",
128
+ width: 16,
129
+ align: "right",
130
+ render: (r) => (r.inodesTotal ? `${percent(r.inodesUsed / r.inodesTotal)} of ${(r.inodesTotal / 1e6).toFixed(1)}M` : "-"),
131
+ color: theme.muted,
132
+ },
133
+ ],
134
+ });
135
+ });
136
+ }
@@ -0,0 +1,85 @@
1
+ import type { Container, Theme } from "@profullstack/hqtui";
2
+ import type { DemoState } from "../state.ts";
3
+
4
+ /** Who is on this machine, who has been, and who failed to get in. */
5
+ export function sessionsScreen(ui: Container, state: DemoState, theme: Theme): void {
6
+ const t = state.sample.telemetry;
7
+
8
+ ui.row({ size: 9, gap: 1 }, (row) => {
9
+ row.panel({ title: "Active Sessions", subtitle: String(t.sessions.length), borderColor: theme.success }, (p) => {
10
+ if (t.sessions.length === 0) {
11
+ p.label("No interactive sessions.");
12
+ p.label("(`who` reports nothing on this host)", { size: 1 });
13
+ return;
14
+ }
15
+ p.table({
16
+ rows: t.sessions,
17
+ columns: [
18
+ { key: "user", title: "User", width: 12, color: theme.primary },
19
+ { key: "tty", title: "TTY", width: 10 },
20
+ { key: "from", title: "From", min: 12, color: theme.accent },
21
+ { key: "loginAt", title: "Login", width: 14, color: theme.muted },
22
+ { key: "idle", title: "Idle", width: 8, align: "right" },
23
+ ],
24
+ });
25
+ });
26
+ row.panel({ title: "Process States", width: 34, borderColor: theme.primary }, (p) => {
27
+ const s = t.states;
28
+ p.meter({ label: "run ", value: s.total ? s.running / s.total : 0, text: String(s.running), heat: false, color: theme.success });
29
+ p.meter({ label: "slp ", value: s.total ? s.sleeping / s.total : 0, text: String(s.sleeping), heat: false, color: theme.primary });
30
+ p.meter({ label: "stop", value: s.total ? s.stopped / s.total : 0, text: String(s.stopped), heat: false, color: theme.warning });
31
+ p.meter({ label: "zomb", value: s.total ? s.zombie / s.total : 0, text: String(s.zombie), heat: false, color: theme.danger });
32
+ p.spacer(1);
33
+ p.keyValues([{ label: "Total", value: String(s.total), color: theme.accent }]);
34
+ });
35
+ });
36
+
37
+ ui.row({ size: "1fr", gap: 1 }, (row) => {
38
+ row.panel({ title: "Recent Logins", subtitle: `${t.logins.length} from wtmp`, borderColor: theme.accent }, (p) => {
39
+ if (t.logins.length === 0) {
40
+ p.label("No login history available.");
41
+ return;
42
+ }
43
+ p.table({
44
+ rows: t.logins,
45
+ selected: state.selected % Math.max(1, t.logins.length),
46
+ zebra: true,
47
+ scrollbar: true,
48
+ columns: [
49
+ { key: "user", title: "User", width: 12, color: theme.primary },
50
+ { key: "tty", title: "TTY", width: 12, color: theme.muted },
51
+ { key: "from", title: "From", min: 14, color: theme.accent },
52
+ { key: "when", title: "When", min: 16, color: theme.muted },
53
+ {
54
+ key: "status",
55
+ title: "Status",
56
+ width: 8,
57
+ color: (row) => (row.status === "still" ? theme.success : theme.muted),
58
+ },
59
+ ],
60
+ });
61
+ });
62
+
63
+ row.column({ width: "0.8fr", gap: 1 }, (column) => {
64
+ column.panel({ title: "Failed Logins", borderColor: theme.danger }, (p) => {
65
+ if (t.failedLogins.length === 0) {
66
+ p.label("None recorded.");
67
+ p.label("(btmp is usually root-only)", { size: 1 });
68
+ return;
69
+ }
70
+ p.table({
71
+ rows: t.failedLogins,
72
+ columns: [
73
+ { key: "user", title: "User", width: 12, color: theme.danger },
74
+ { key: "from", title: "From", min: 12 },
75
+ { key: "when", title: "When", min: 14, color: theme.muted },
76
+ ],
77
+ });
78
+ });
79
+ column.panel({ title: "Session History", size: 8, borderColor: theme.secondary }, (p) => {
80
+ p.label("concurrent sessions", { size: 1 });
81
+ p.graph({ values: t.sessionHistory, min: 0, fill: true, color: theme.success });
82
+ });
83
+ });
84
+ });
85
+ }
@@ -0,0 +1,199 @@
1
+ import type { Container, Theme } from "@profullstack/hqtui";
2
+ import { seriesColor } from "@profullstack/hqtui";
3
+ import type { DemoState } from "../state.ts";
4
+ import { num, percent } from "../format.ts";
5
+
6
+ function rate(value: number): string {
7
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M/s`;
8
+ if (value >= 1000) return `${(value / 1000).toFixed(1)}K/s`;
9
+ return `${value.toFixed(0)}/s`;
10
+ }
11
+
12
+ const STATUS_COLORS = (theme: Theme): Record<string, number> => ({
13
+ "2xx": theme.success,
14
+ "3xx": theme.accent,
15
+ "4xx": theme.warning,
16
+ "5xx": theme.danger,
17
+ "1xx": theme.secondary,
18
+ });
19
+
20
+ /** Every protocol in and out of this host: sockets, TCP counters, SSH, HTTP. */
21
+ export function trafficScreen(ui: Container, state: DemoState, theme: Theme): void {
22
+ const t = state.sample.telemetry;
23
+ const net = t.net;
24
+ const http = t.http;
25
+
26
+ ui.row({ size: 13, gap: 1 }, (row) => {
27
+ row.panel({
28
+ title: "Protocols",
29
+ subtitle: `${t.inboundConnections} in / ${t.outboundConnections} out`,
30
+ borderColor: theme.accent,
31
+ }, (p) => {
32
+ if (t.protocols.length === 0) {
33
+ p.label("No sockets visible.");
34
+ return;
35
+ }
36
+ const max = Math.max(1, ...t.protocols.map((x) => x.total));
37
+ p.meters(
38
+ t.protocols.slice(0, 9).map((bucket, i) => ({
39
+ label: bucket.protocol,
40
+ value: bucket.total / max,
41
+ color: seriesColor(theme, i),
42
+ text: `${bucket.total}`,
43
+ })),
44
+ { labelWidth: 13, valueWidth: 5 },
45
+ );
46
+ });
47
+
48
+ row.panel({ title: "TCP", width: "0.9fr", borderColor: theme.primary }, (p) => {
49
+ p.row({ size: 1 }, (r) => {
50
+ r.text(`↓ ${rate(net.rates.inSegs)} seg`, { fg: theme.primary });
51
+ r.text(`↑ ${rate(net.rates.outSegs)} seg`, { fg: theme.secondary, align: "right" });
52
+ });
53
+ p.graph({
54
+ series: [
55
+ { values: t.netInHistory, color: theme.primary, fill: true },
56
+ { values: t.netOutHistory, color: theme.secondary, fill: true },
57
+ ],
58
+ min: 0,
59
+ size: "1fr",
60
+ });
61
+ p.divider();
62
+ p.keyValues([
63
+ { label: "Established", value: String(net.tcpEstablished), color: theme.success },
64
+ { label: "Opens in/out", value: `${rate(net.rates.passiveOpens)} / ${rate(net.rates.activeOpens)}`, color: theme.accent },
65
+ { label: "Resets sent", value: net.tcpOutRsts.toLocaleString(), color: theme.muted },
66
+ ]);
67
+ });
68
+
69
+ row.panel({
70
+ title: "Retransmits",
71
+ width: "0.7fr",
72
+ borderColor: net.retransRatio > 0.02 ? theme.danger : theme.success,
73
+ }, (p) => {
74
+ p.text(percent(net.retransRatio, 2), {
75
+ fg: net.retransRatio > 0.02 ? theme.danger : theme.success,
76
+ bold: true,
77
+ size: 1,
78
+ });
79
+ p.label("of outbound segments", { size: 1 });
80
+ p.graph({ values: t.retransHistory, min: 0, fill: true, color: theme.danger, size: "1fr" });
81
+ p.keyValues([
82
+ { label: "UDP in/out", value: `${rate(net.rates.udpIn)} / ${rate(net.rates.udpOut)}`, color: theme.muted },
83
+ { label: "ICMP", value: `${net.icmpInMsgs} / ${net.icmpOutMsgs}`, color: theme.muted },
84
+ ]);
85
+ });
86
+ });
87
+
88
+ ui.row({ size: "1fr", gap: 1 }, (row) => {
89
+ row.column({ gap: 1 }, (column) => {
90
+ column.panel({
91
+ title: "HTTP",
92
+ subtitle: http ? `${num(http.requestsPerSecond, 1)} req/s` : "no access log",
93
+ borderColor: theme.success,
94
+ }, (p) => {
95
+ if (!http) {
96
+ p.label("No readable HTTP access log.");
97
+ p.label("nginx, apache, httpd and caddy logs are", { size: 1 });
98
+ p.label("root/adm readable — run with sudo to track requests.", { size: 1 });
99
+ return;
100
+ }
101
+ p.row({ size: 1 }, (r) => {
102
+ r.text(http.source, { fg: theme.muted });
103
+ r.text(`${http.upgrades} upgrades (ws)`, { fg: theme.secondary, align: "right" });
104
+ });
105
+ p.graph({ values: http.history, min: 0, fill: true, color: theme.success, size: 6 });
106
+ p.divider({ label: "status" });
107
+ const colors = STATUS_COLORS(theme);
108
+ const max = Math.max(1, ...http.statusClasses.map((s) => s.count));
109
+ p.meters(
110
+ http.statusClasses.map((entry) => ({
111
+ label: entry.class,
112
+ value: entry.count / max,
113
+ color: colors[entry.class] ?? theme.muted,
114
+ text: String(entry.count),
115
+ })),
116
+ { labelWidth: 5, valueWidth: 7 },
117
+ );
118
+ p.divider({ label: "top paths" });
119
+ p.table({
120
+ rows: http.topPaths,
121
+ header: false,
122
+ columns: [
123
+ { key: "path", title: "Path", min: 20, color: theme.primary },
124
+ { key: "count", title: "Hits", width: 7, align: "right", color: theme.accent },
125
+ ],
126
+ });
127
+ });
128
+ });
129
+
130
+ row.column({ width: "0.85fr", gap: 1 }, (column) => {
131
+ column.panel({ title: "SSH Activity", subtitle: String(t.ssh.length), borderColor: theme.warning }, (p) => {
132
+ if (t.ssh.length === 0) {
133
+ p.label("No sshd events in the journal.");
134
+ return;
135
+ }
136
+ p.table({
137
+ rows: [...t.ssh].reverse(),
138
+ zebra: true,
139
+ scrollbar: true,
140
+ columns: [
141
+ { key: "time", title: "Time", width: 9, color: theme.muted },
142
+ {
143
+ key: "action",
144
+ title: "Action",
145
+ width: 11,
146
+ color: (row) =>
147
+ row.action === "accepted" ? theme.success
148
+ : row.action === "disconnect" ? theme.muted
149
+ : theme.danger,
150
+ },
151
+ { key: "user", title: "User", width: 12, color: theme.primary },
152
+ { key: "from", title: "From", min: 14, color: theme.accent },
153
+ { key: "method", title: "Method", width: 10, color: theme.muted },
154
+ ],
155
+ });
156
+ });
157
+
158
+ column.panel({ title: "Top Remote Hosts", size: 10, borderColor: theme.secondary }, (p) => {
159
+ if (t.remotes.length === 0) {
160
+ p.label("No remote peers.");
161
+ return;
162
+ }
163
+ p.table({
164
+ rows: t.remotes,
165
+ zebra: true,
166
+ columns: [
167
+ { key: "host", title: "Host", min: 16, color: theme.accent },
168
+ { key: "connections", title: "Conns", width: 6, align: "right", color: theme.success },
169
+ { key: "protocols", title: "Protocols", min: 12, color: theme.muted },
170
+ ],
171
+ });
172
+ });
173
+ });
174
+ });
175
+
176
+ if (http && http.recent.length) {
177
+ ui.panel({ title: "Recent Requests", size: 10, borderColor: theme.primary }, (p) => {
178
+ p.table({
179
+ rows: http.recent,
180
+ zebra: true,
181
+ scrollbar: true,
182
+ columns: [
183
+ { key: "time", title: "Time", width: 9, color: theme.muted },
184
+ { key: "method", title: "Method", width: 7, color: theme.secondary },
185
+ { key: "path", title: "Path", min: 24, color: theme.primary },
186
+ {
187
+ key: "status",
188
+ title: "Status",
189
+ width: 7,
190
+ align: "right",
191
+ color: (row) => STATUS_COLORS(theme)[`${String(row.status)[0]}xx`] ?? theme.muted,
192
+ },
193
+ { key: "client", title: "Client", width: 16, color: theme.accent },
194
+ { key: "bytes", title: "Bytes", width: 9, align: "right", color: theme.muted },
195
+ ],
196
+ });
197
+ });
198
+ }
199
+ }
package/src/simulation.ts CHANGED
@@ -23,8 +23,15 @@ export interface ProcessSample {
23
23
  state: string;
24
24
  }
25
25
 
26
+ import { type Telemetry, emptyTelemetry } from "./system/telemetry.ts";
27
+ import { updateTelemetry } from "./system/simulated-telemetry.ts";
28
+
29
+ export type { Telemetry };
30
+
26
31
  export interface SystemSample {
27
32
  time: number;
33
+ /** Everything beyond classic CPU/memory/disk monitoring. */
34
+ telemetry: Telemetry;
28
35
  cpu: {
29
36
  total: number;
30
37
  cores: number[];
@@ -153,6 +160,7 @@ export class SystemSimulation {
153
160
  const totalMemory = 16 * 1024 ** 3;
154
161
  this.state = {
155
162
  time: 0,
163
+ telemetry: emptyTelemetry(),
156
164
  cpu: {
157
165
  total: 0.18,
158
166
  cores: new Array(cores).fill(0.15),
@@ -332,6 +340,10 @@ export class SystemSimulation {
332
340
  { label: "GPU Power", value: `${(4 + s.cpu.total * 9).toFixed(1)} W` },
333
341
  ];
334
342
 
343
+ updateTelemetry(s.telemetry, this.random, this.tick, s.cpu.total);
344
+ s.system.processCount = s.telemetry.states.total;
345
+ s.system.threadCount = 940 + Math.round(s.cpu.total * 120);
346
+
335
347
  s.system.uptime += dt;
336
348
  s.system.threadCount = 940 + Math.round(s.cpu.total * 120);
337
349
  s.system.contextSwitches = Math.round(48000 + s.cpu.total * 20000);
package/src/state.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import type { Theme } from "@profullstack/hqtui";
2
2
  import type { SystemSample } from "./system/index.ts";
3
3
 
4
- export type ScreenName = "dashboard" | "components" | "graphics" | "themes" | "input" | "stress";
4
+ export type ScreenName =
5
+ | "dashboard" | "traffic" | "sessions" | "network" | "services"
6
+ | "components" | "graphics" | "themes" | "input" | "stress";
5
7
 
6
- export const SCREENS: ScreenName[] = ["dashboard", "components", "graphics", "themes", "input", "stress"];
8
+ export const SCREENS: ScreenName[] = [
9
+ "dashboard", "traffic", "sessions", "network", "services",
10
+ "components", "graphics", "themes", "input", "stress",
11
+ ];
7
12
 
8
13
  export interface DemoState {
9
14
  sample: SystemSample;
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import os from "node:os";
4
4
  import type { SystemSample } from "../simulation.ts";
5
+ import { emptyTelemetry } from "./telemetry.ts";
5
6
 
6
7
  const run = promisify(execFile);
7
8
 
@@ -31,6 +32,7 @@ export function baseSample(): SystemSample {
31
32
  const total = os.totalmem();
32
33
  return {
33
34
  time: 0,
35
+ telemetry: emptyTelemetry(),
34
36
  cpu: {
35
37
  total: 0,
36
38
  cores: new Array(Math.max(1, cpus.length)).fill(0),