pyre-cli 9.5.0 → 9.5.2

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.
@@ -15,7 +15,9 @@ import chalk from 'chalk';
15
15
  import os from 'node:os';
16
16
  import { run } from './run.js';
17
17
  import { getSmcMetrics, parseSuffix } from './smc.js';
18
- import type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData, PacketData, NetworkProcess, TaskData } from './types.js';
18
+ import { resolveIp } from './dns_cache.js';
19
+ import type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData, PacketData, NetworkProcess, TaskData, ContainerData, NetworkConnection, BlenderRenderData, ProtocolStats, ConnectionStateStats, RemoteHostInfo } from './types.js';
20
+ import { collectBlenderRenders } from './blender.js';
19
21
 
20
22
  const SP_TTL_MS = 10_000;
21
23
  const NETSTAT_TTL_MS = 1000;
@@ -30,6 +32,8 @@ let netstatIbCache: { raw: string; ts: number } | null = null;
30
32
  let routeCache: { iface: string; ip: string; ts: number } | null = null;
31
33
  const sysctlCache = new Map<string, { value: string; ts: number }>();
32
34
 
35
+ let prevNetSample: { rxBytes: number; txBytes: number; rxPackets: number; txPackets: number; ts: number } | null = null;
36
+
33
37
  async function cachedSysctl(key: string, fallback: string): Promise<string> {
34
38
  const now = Date.now();
35
39
  const hit = sysctlCache.get(key);
@@ -127,7 +131,7 @@ function estimateBatteryLife(level: number, state: string): { estimatedTimeToEmp
127
131
  * complete {@link StatsData} snapshot.
128
132
  */
129
133
  export async function collectAll(opts: { detailed?: boolean; processLimit?: number } = {}): Promise<StatsData> {
130
- const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power, packets, tasks] = await Promise.all([
134
+ const [cpu, gpu, memory, disk, battery, thermal, network, processes, system, power, packets, tasks, containers, blenderRenders] = await Promise.all([
131
135
  collectCpu(),
132
136
  collectGpu(opts.detailed),
133
137
  collectMemory(),
@@ -140,6 +144,8 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
140
144
  collectPower(),
141
145
  collectPackets(),
142
146
  collectTasks(),
147
+ collectContainers(),
148
+ collectBlenderRenders(),
143
149
  ]);
144
150
 
145
151
  return {
@@ -161,9 +167,34 @@ export async function collectAll(opts: { detailed?: boolean; processLimit?: numb
161
167
  timestamp: new Date().toISOString(),
162
168
  packets,
163
169
  tasks,
170
+ containers,
171
+ blenderRenders,
164
172
  };
165
173
  }
166
174
 
175
+ export async function collectContainers(): Promise<ContainerData[]> {
176
+ try {
177
+ const raw = (await run('docker stats --no-stream --format "{{.ID}}|{{.Name}}|{{.CPUPerc}}|{{.MemUsage}}|{{.MemPerc}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}" 2>/dev/null', '')).trim();
178
+ if (!raw) return [];
179
+
180
+ return raw.split('\n').map(line => {
181
+ const parts = line.split('|');
182
+ return {
183
+ id: parts[0],
184
+ name: parts[1],
185
+ cpuPercent: parseFloat(parts[2].replace('%', '')) || 0,
186
+ memUsage: parts[3],
187
+ memPercent: parseFloat(parts[4].replace('%', '')) || 0,
188
+ netIO: parts[5],
189
+ blockIO: parts[6],
190
+ pids: parseInt(parts[7], 10) || 0
191
+ };
192
+ });
193
+ } catch {
194
+ return [];
195
+ }
196
+ }
197
+
167
198
  /**
168
199
  * Read power-draw metrics (CPU watts, GPU watts, combined)
169
200
  * from the SMC via powermetrics. Returns `null` when no
@@ -829,9 +860,166 @@ export async function collectNetwork(): Promise<NetworkData> {
829
860
  }
830
861
  }
831
862
 
832
- return { interface: iface, ip, rxBytes, txBytes, rxPackets, txPackets };
863
+ const now = Date.now();
864
+ let rxRate = 0;
865
+ let txRate = 0;
866
+ if (prevNetSample && prevNetSample.ts > 0) {
867
+ const dt = Math.max((now - prevNetSample.ts) / 1000, 0.001);
868
+ rxRate = Math.max(0, (rxBytes - prevNetSample.rxBytes) / dt);
869
+ txRate = Math.max(0, (txBytes - prevNetSample.txBytes) / dt);
870
+ }
871
+ prevNetSample = { rxBytes, txBytes, rxPackets, txPackets, ts: now };
872
+
873
+ const protocolStats = await getProtocolStats();
874
+ const connectionStates = await getConnectionStates();
875
+ const listeningPorts = await getListeningPorts();
876
+ const establishedConnections = connectionStates.established;
877
+ const topRemoteHosts = await getTopRemoteHosts();
878
+
879
+ return {
880
+ interface: iface,
881
+ ip,
882
+ rxBytes,
883
+ txBytes,
884
+ rxPackets,
885
+ txPackets,
886
+ rxRate,
887
+ txRate,
888
+ connections: establishedConnections,
889
+ protocols: protocolStats,
890
+ connectionStates,
891
+ listeningPorts,
892
+ establishedConnections,
893
+ topRemoteHosts,
894
+ };
895
+ } catch {
896
+ return {
897
+ interface: 'unknown',
898
+ ip: '0.0.0.0',
899
+ rxBytes: 0,
900
+ txBytes: 0,
901
+ rxPackets: 0,
902
+ txPackets: 0,
903
+ rxRate: 0,
904
+ txRate: 0,
905
+ connections: 0,
906
+ protocols: { tcp: { connections: 0, rxBytes: 0, txBytes: 0 }, udp: { connections: 0, rxBytes: 0, txBytes: 0 } },
907
+ connectionStates: { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 },
908
+ listeningPorts: [],
909
+ establishedConnections: 0,
910
+ topRemoteHosts: [],
911
+ };
912
+ }
913
+ }
914
+
915
+ async function getProtocolStats(): Promise<ProtocolStats> {
916
+ try {
917
+ const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | awk '{print $1}' | sort | uniq -c", '');
918
+ const tcpConnections = (await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | wc -l", '0')).trim();
919
+ const udpConnections = (await run("netstat -n -p udp 2>/dev/null | tail -n +3 | wc -l", '0')).trim();
920
+
921
+ let tcpRxBytes = 0;
922
+ let tcpTxBytes = 0;
923
+ let udpRxBytes = 0;
924
+ let udpTxBytes = 0;
925
+
926
+ try {
927
+ const netstat = await cachedNetstatIb();
928
+ for (const line of netstat.split('\n')) {
929
+ const parts = line.split(/\s+/);
930
+ if (parts.length >= 9 && parts[0] && !parts[0].startsWith('Name')) {
931
+ const iface = parts[0];
932
+ if (iface.includes('lo')) continue;
933
+ const rx = parseInt(parts[5]) || 0;
934
+ const tx = parseInt(parts[7]) || 0;
935
+ tcpRxBytes += rx;
936
+ tcpTxBytes += tx;
937
+ }
938
+ }
939
+ } catch {
940
+ // ignore
941
+ }
942
+
943
+ return {
944
+ tcp: {
945
+ connections: parseInt(tcpConnections) || 0,
946
+ rxBytes: tcpRxBytes,
947
+ txBytes: tcpTxBytes,
948
+ },
949
+ udp: {
950
+ connections: parseInt(udpConnections) || 0,
951
+ rxBytes: udpRxBytes,
952
+ txBytes: udpTxBytes,
953
+ },
954
+ };
955
+ } catch {
956
+ return { tcp: { connections: 0, rxBytes: 0, txBytes: 0 }, udp: { connections: 0, rxBytes: 0, txBytes: 0 } };
957
+ }
958
+ }
959
+
960
+ async function getConnectionStates(): Promise<ConnectionStateStats> {
961
+ try {
962
+ const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | awk '{print $6}' | sort | uniq -c", '');
963
+ const lines = raw.split('\n').filter(Boolean);
964
+ const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
965
+
966
+ for (const line of lines) {
967
+ const match = line.match(/(\d+)\s+(\S+)/);
968
+ if (!match) continue;
969
+ const count = parseInt(match[1]);
970
+ const state = match[2].toUpperCase();
971
+
972
+ if (state.includes('ESTABLISHED')) stats.established += count;
973
+ else if (state.includes('LISTEN')) stats.listening += count;
974
+ else if (state.includes('TIME_WAIT')) stats.timeWait += count;
975
+ else if (state.includes('CLOSE_WAIT')) stats.closeWait += count;
976
+ else stats.other += count;
977
+ }
978
+
979
+ return stats;
980
+ } catch {
981
+ return { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
982
+ }
983
+ }
984
+
985
+ async function getListeningPorts(): Promise<number[]> {
986
+ try {
987
+ const raw = await run("netstat -n -p tcp 2>/dev/null | grep LISTEN | awk '{print $4}' | rev | cut -d. -f1 | rev", '');
988
+ const ports = raw.split('\n')
989
+ .map(p => parseInt(p, 10))
990
+ .filter(p => !isNaN(p) && p > 0);
991
+ return [...new Set(ports)].sort((a, b) => a - b);
992
+ } catch {
993
+ return [];
994
+ }
995
+ }
996
+
997
+ async function getTopRemoteHosts(): Promise<RemoteHostInfo[]> {
998
+ try {
999
+ const raw = await run("netstat -n -p tcp 2>/dev/null | tail -n +3 | grep ESTABLISHED | awk '{print $5}' | rev | cut -d. -f1 | rev", '');
1000
+ const ports = raw.split('\n').filter(Boolean);
1001
+ const hostMap = new Map<string, { count: number; bytes: number }>();
1002
+
1003
+ for (const port of ports) {
1004
+ const host = port.split(':')[0];
1005
+ if (!host || host === '127.0.0.1' || host === '::1') continue;
1006
+ const existing = hostMap.get(host) || { count: 0, bytes: 0 };
1007
+ hostMap.set(host, { count: existing.count + 1, bytes: existing.bytes });
1008
+ }
1009
+
1010
+ const result: RemoteHostInfo[] = [];
1011
+ for (const [host, info] of hostMap.entries()) {
1012
+ result.push({
1013
+ host,
1014
+ ip: host,
1015
+ bytesTransferred: info.bytes,
1016
+ connectionCount: info.count,
1017
+ });
1018
+ }
1019
+
1020
+ return result.sort((a, b) => b.connectionCount - a.connectionCount).slice(0, 10);
833
1021
  } catch {
834
- return { interface: 'unknown', ip: '0.0.0.0', rxBytes: 0, txBytes: 0, rxPackets: 0, txPackets: 0 };
1022
+ return [];
835
1023
  }
836
1024
  }
837
1025
 
@@ -886,47 +1074,106 @@ export async function collectPackets(): Promise<PacketData | null> {
886
1074
  }
887
1075
 
888
1076
  let connections = 0;
1077
+ let allProcesses: NetworkProcess[] = [];
1078
+
889
1079
  try {
890
- const tcpConns = (await run('netstat -an | grep ESTABLISHED | wc -l', '0', 2000)).trim();
891
- connections = parseInt(tcpConns) || 0;
892
- } catch {
893
- connections = 0;
894
- }
895
-
896
- let topProcesses: { pid: number; command: string; rxBytes: number; txBytes: number }[] = [];
897
- let allProcesses: { pid: number; command: string; rxBytes: number; txBytes: number; rxPackets?: number; txPackets?: number }[] = [];
898
- try {
899
- const lsof = (await run('lsof -i -n -P 2>/dev/null | tail -n +2 | awk \'{print $1, $2}\' | sort | uniq -c | sort -rn', '', 2000)).trim();
900
- const lsofLines = lsof.split('\n');
901
- topProcesses = lsofLines.map(line => {
902
- const parts = line.trim().split(/\s+/);
903
- if (parts.length >= 3) {
904
- return { pid: parseInt(parts[2]) || 0, command: parts[1], rxBytes: 0, txBytes: parseInt(parts[0]) || 0 };
1080
+ const nettop = (await run('nettop -L 1 -J bytes_in,bytes_out,state 2>/dev/null', '', 2000)).trim();
1081
+ const rows = nettop.split('\n');
1082
+
1083
+ let currentProc: NetworkProcess | null = null;
1084
+
1085
+ for (const row of rows) {
1086
+ if (!row || row.startsWith(',state,')) continue;
1087
+
1088
+ const cols = row.split(',');
1089
+ const nameCol = cols[0];
1090
+
1091
+ // If it doesn't contain <->, it's a process row
1092
+ if (!nameCol.includes('<->')) {
1093
+ if (currentProc) {
1094
+ if (currentProc.connections && currentProc.connections.length > 0) {
1095
+ allProcesses.push(currentProc);
1096
+ }
905
1097
  }
906
- return { pid: 0, command: '', rxBytes: 0, txBytes: 0 };
907
- }).filter(p => p.pid > 0);
908
-
909
- allProcesses = topProcesses;
1098
+
1099
+ // Extract PID and Name (e.g., "syslogd.369")
1100
+ const lastDot = nameCol.lastIndexOf('.');
1101
+ let pid = 0;
1102
+ let command = nameCol;
1103
+ if (lastDot > 0) {
1104
+ const possiblePid = parseInt(nameCol.substring(lastDot + 1));
1105
+ if (!isNaN(possiblePid)) {
1106
+ pid = possiblePid;
1107
+ command = nameCol.substring(0, lastDot);
1108
+ }
1109
+ }
1110
+
1111
+ const rx = parseInt(cols[2]) || 0;
1112
+ const tx = parseInt(cols[3]) || 0;
1113
+
1114
+ currentProc = { pid, command, rxBytes: rx, txBytes: tx, connections: [] };
1115
+ } else {
1116
+ // Connection row
1117
+ if (currentProc) {
1118
+ const state = cols[1];
1119
+ const rx = parseInt(cols[2]) || 0;
1120
+ const tx = parseInt(cols[3]) || 0;
1121
+ if (state === 'Established') connections++;
1122
+
1123
+ // Parse endpoints (e.g. "tcp4 172.16.0.2:49844<->17.242.13.5:5223")
1124
+ const spaceIdx = nameCol.indexOf(' ');
1125
+ const protocol = spaceIdx > 0 ? nameCol.substring(0, spaceIdx) : '';
1126
+ const endpoints = spaceIdx > 0 ? nameCol.substring(spaceIdx + 1) : nameCol;
1127
+ const arrowIdx = endpoints.indexOf('<->');
1128
+ let local = '';
1129
+ let remote = '';
1130
+ if (arrowIdx > 0) {
1131
+ local = endpoints.substring(0, arrowIdx);
1132
+ remote = endpoints.substring(arrowIdx + 3);
1133
+
1134
+ // Attempt DNS resolution on remote IP
1135
+ remote = resolveIp(remote);
1136
+ } else {
1137
+ local = endpoints;
1138
+ }
1139
+
1140
+ // Ignore purely local communication if you want, but for now include all
1141
+ currentProc.connections!.push({ protocol, local, remote, state, rxBytes: rx, txBytes: tx });
1142
+ }
1143
+ }
1144
+ }
1145
+ if (currentProc && currentProc.connections && currentProc.connections.length > 0) {
1146
+ allProcesses.push(currentProc);
1147
+ }
910
1148
  } catch {
911
- topProcesses = [];
912
1149
  allProcesses = [];
913
1150
  }
914
-
915
- return {
916
- totalPackets: totalRxPackets + totalTxPackets,
917
- rxPackets: totalRxPackets,
918
- txPackets: totalTxPackets,
919
- rxRate: 0,
920
- txRate: 0,
921
- connections,
922
- topProcesses,
923
- interfaces: ifaceStats,
924
- allProcesses,
925
- };
926
- } catch {
927
- return null;
928
- }
929
- }
1151
+
1152
+ // Sort processes by total bandwidth (tx + rx) to get top processes
1153
+ const topProcesses = [...allProcesses].sort((a, b) => (b.rxBytes + b.txBytes) - (a.rxBytes + a.txBytes));
1154
+
1155
+ const protocolStats = await getProtocolStats();
1156
+ const connectionStates = await getConnectionStates();
1157
+ const listeningPorts = await getListeningPorts();
1158
+
1159
+ return {
1160
+ totalPackets: totalRxPackets + totalTxPackets,
1161
+ rxPackets: totalRxPackets,
1162
+ txPackets: totalTxPackets,
1163
+ rxRate: 0,
1164
+ txRate: 0,
1165
+ connections,
1166
+ topProcesses,
1167
+ interfaces: ifaceStats,
1168
+ allProcesses,
1169
+ protocolStats,
1170
+ connectionStates,
1171
+ listeningPorts,
1172
+ };
1173
+ } catch {
1174
+ return null;
1175
+ }
1176
+ }
930
1177
 
931
1178
  export async function collectTasks(limit = 12): Promise<TaskData[]> {
932
1179
  try {
@@ -0,0 +1,48 @@
1
+ import dns from 'node:dns';
2
+
3
+ const cache = new Map<string, string>();
4
+ const pending = new Set<string>();
5
+
6
+ export function resolveIp(ip: string): string {
7
+ // Extract IP if it has port (e.g., 17.242.13.5:5223 or [2606:4700...]:443)
8
+ let cleanIp = ip;
9
+ if (cleanIp.includes(']:')) {
10
+ cleanIp = cleanIp.substring(1, cleanIp.indexOf(']'));
11
+ } else if (cleanIp.includes(':') && cleanIp.split(':').length === 2) {
12
+ cleanIp = cleanIp.split(':')[0];
13
+ } else if (cleanIp.includes(':') && !cleanIp.includes(']')) {
14
+ // ipv6 without port or brackets?
15
+ // If there's more than one colon, it's ipv6.
16
+ const lastColon = cleanIp.lastIndexOf(':');
17
+ const isPort = !cleanIp.includes(':', cleanIp.indexOf(':') + 1); // Only 1 colon means it's a port. Wait, ipv6 has many colons.
18
+ // E.g., 2606:4700:103::2.443 -> nettop uses `.` for port in ipv6!
19
+ }
20
+
21
+ // Handle nettop's IPv6 port format: 2606:4700:103::2.443 (the last dot separates the port)
22
+ if (cleanIp.includes(':') && cleanIp.includes('.')) {
23
+ const lastDot = cleanIp.lastIndexOf('.');
24
+ if (lastDot > cleanIp.lastIndexOf(':')) {
25
+ cleanIp = cleanIp.substring(0, lastDot);
26
+ }
27
+ }
28
+
29
+ if (cache.has(cleanIp)) return cache.get(cleanIp)!;
30
+ if (pending.has(cleanIp)) return ip;
31
+
32
+ // Don't resolve local/multicast/broadcast
33
+ if (cleanIp.startsWith('127.') || cleanIp.startsWith('192.168.') || cleanIp.startsWith('10.') || cleanIp.startsWith('172.16.') || cleanIp.startsWith('ff02:') || cleanIp === '*:*' || cleanIp === '*' || cleanIp.startsWith('fe80:')) {
34
+ return ip;
35
+ }
36
+
37
+ pending.add(cleanIp);
38
+ dns.reverse(cleanIp, (err, hostnames) => {
39
+ if (!err && hostnames && hostnames.length > 0) {
40
+ cache.set(cleanIp, hostnames[0]);
41
+ } else {
42
+ cache.set(cleanIp, cleanIp); // don't try again
43
+ }
44
+ pending.delete(cleanIp);
45
+ });
46
+
47
+ return ip;
48
+ }
@@ -10,5 +10,6 @@
10
10
  * - `collectors.ts` — individual metric collectors and the orchestrator
11
11
  * - `run.ts` — shared shell-execution helper
12
12
  */
13
- export { collectAll, collectPower, collectPackets, collectTasks } from './collectors.js';
14
- export type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData, PacketData, NetworkProcess, TaskData } from './types.js';
13
+ export { collectAll, collectPower, collectPackets, collectTasks, collectContainers } from './collectors.js';
14
+ export { collectBlenderRenders } from './blender.js';
15
+ export type { StatsData, CpuData, MemoryData, ThermalData, BatteryData, PowerData, DiskData, NetworkData, ProcessData, GpuData, PacketData, NetworkProcess, TaskData, ContainerData, BlenderRenderData } from './types.js';
@@ -99,6 +99,33 @@ export async function getSmcMetrics(): Promise<SmcMetrics> {
99
99
  }
100
100
  }
101
101
 
102
+ // 2b. AppleSmartBattery fallback — works without sudo on all MacBooks
103
+ // VirtualTemperature reflects the SoC/die temperature; Temperature is
104
+ // the battery cell temperature. Both are reported in centi-Celsius
105
+ // (e.g. 3539 → 35.39 °C).
106
+ if (result.temps['cpu_die'] === undefined) {
107
+ try {
108
+ const battRaw = await run('ioreg -r -n AppleSmartBattery 2>/dev/null', '');
109
+ if (battRaw) {
110
+ // Prefer VirtualTemperature (closer to die temp) over Temperature (battery cell)
111
+ const virtMatch = battRaw.match(/"VirtualTemperature"\s*=\s*(\d+)/);
112
+ const cellMatch = battRaw.match(/"Temperature"\s*=\s*(\d+)/);
113
+ const rawVirt = virtMatch ? parseFloat(virtMatch[1]) : undefined;
114
+ const rawCell = cellMatch ? parseFloat(cellMatch[1]) : undefined;
115
+ // Convert centi-Celsius to Celsius (e.g. 3085 → 30.85 → 30.9)
116
+ const virtC = rawVirt !== undefined ? Math.round(rawVirt / 10) / 10 : undefined;
117
+ const cellC = rawCell !== undefined ? Math.round(rawCell / 10) / 10 : undefined;
118
+ if (virtC !== undefined && virtC >= 15 && virtC <= 115) {
119
+ result.temps['cpu_die'] = virtC;
120
+ } else if (cellC !== undefined && cellC >= 15 && cellC <= 115) {
121
+ result.temps['cpu_die'] = cellC;
122
+ }
123
+ }
124
+ } catch {
125
+ // ignore
126
+ }
127
+ }
128
+
102
129
  // 3. Linux /sys/class/thermal or /sys/class/hwmon fallback
103
130
  if (result.temps['cpu_die'] === undefined) {
104
131
  try {
@@ -21,6 +21,19 @@ export interface StatsData {
21
21
  timestamp: string;
22
22
  packets: PacketData | null;
23
23
  tasks: TaskData[];
24
+ containers?: ContainerData[];
25
+ blenderRenders?: BlenderRenderData[];
26
+ }
27
+
28
+ export interface ContainerData {
29
+ id: string;
30
+ name: string;
31
+ cpuPercent: number;
32
+ memUsage: string;
33
+ memPercent: number;
34
+ netIO: string;
35
+ blockIO: string;
36
+ pids: number;
24
37
  }
25
38
 
26
39
  export interface GpuData {
@@ -38,18 +51,20 @@ export interface GpuData {
38
51
  combinedWatts?: number;
39
52
  }
40
53
 
41
- export interface PacketData {
42
- totalPackets: number;
43
- rxPackets: number;
44
- txPackets: number;
45
- rxRate: number;
46
- txRate: number;
47
- connections: number;
48
- topProcesses: { pid: number; command: string; rxBytes: number; txBytes: number }[];
49
- /** Every process with an open network connection (unlike topProcesses, not truncated). */
50
- allProcesses: { pid: number; command: string; rxBytes: number; txBytes: number; rxPackets?: number; txPackets?: number }[];
51
- interfaces: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[];
52
- }
54
+ export interface PacketData {
55
+ totalPackets: number;
56
+ rxPackets: number;
57
+ txPackets: number;
58
+ rxRate: number;
59
+ txRate: number;
60
+ connections: number;
61
+ topProcesses: NetworkProcess[];
62
+ allProcesses: NetworkProcess[];
63
+ interfaces: { iface: string; rxPackets: number; txPackets: number; rxBytes: number; txBytes: number }[];
64
+ protocolStats?: ProtocolStats;
65
+ connectionStates?: ConnectionStateStats;
66
+ listeningPorts?: number[];
67
+ }
53
68
 
54
69
  export interface TaskData {
55
70
  pid: number;
@@ -138,10 +153,60 @@ export interface NetworkData {
138
153
  txBytes: number;
139
154
  rxPackets: number;
140
155
  txPackets: number;
156
+ rxRate: number;
157
+ txRate: number;
141
158
  rxPacketsPerSec?: number;
142
159
  txPacketsPerSec?: number;
143
160
  connections?: number;
144
161
  topProcesses?: NetworkProcess[];
162
+ protocols?: ProtocolStats;
163
+ connectionStates?: ConnectionStateStats;
164
+ listeningPorts?: number[];
165
+ establishedConnections?: number;
166
+ topRemoteHosts?: RemoteHostInfo[];
167
+ }
168
+
169
+ export interface ProtocolStats {
170
+ tcp: { connections: number; rxBytes: number; txBytes: number };
171
+ udp: { connections: number; rxBytes: number; txBytes: number };
172
+ }
173
+
174
+ export interface ConnectionStateStats {
175
+ established: number;
176
+ listening: number;
177
+ timeWait: number;
178
+ closeWait: number;
179
+ other: number;
180
+ }
181
+
182
+ export interface RemoteHostInfo {
183
+ host: string;
184
+ ip: string;
185
+ country?: string;
186
+ city?: string;
187
+ asn?: string;
188
+ bytesTransferred: number;
189
+ connectionCount: number;
190
+ }
191
+
192
+ export interface DnsQuery {
193
+ timestamp: number;
194
+ domain: string;
195
+ type: string;
196
+ process: string;
197
+ }
198
+
199
+ export interface NetworkConnection {
200
+ protocol: string;
201
+ local: string;
202
+ remote: string;
203
+ state: string;
204
+ rxBytes: number;
205
+ txBytes: number;
206
+ remoteHost?: string;
207
+ country?: string;
208
+ city?: string;
209
+ asn?: string;
145
210
  }
146
211
 
147
212
  export interface NetworkProcess {
@@ -149,8 +214,11 @@ export interface NetworkProcess {
149
214
  command: string;
150
215
  rxBytes: number;
151
216
  txBytes: number;
152
- rxPackets: number;
153
- txPackets: number;
217
+ rxRate?: number;
218
+ txRate?: number;
219
+ rxPackets?: number;
220
+ txPackets?: number;
221
+ connections?: NetworkConnection[];
154
222
  }
155
223
 
156
224
  export interface ProcessData {
@@ -164,4 +232,19 @@ export interface ProcessData {
164
232
  threads: number;
165
233
  runtime: number;
166
234
  isGpuOrMlAttributed?: boolean;
235
+ }
236
+
237
+ export interface BlenderRenderData {
238
+ pid: number;
239
+ blendFile: string;
240
+ renderEngine: string;
241
+ frameStart: number;
242
+ frameEnd: number;
243
+ currentFrame: number;
244
+ totalFrames: number;
245
+ elapsedSec: number;
246
+ completionPercent: number;
247
+ status: 'rendering' | 'finalizing' | 'loading' | 'idle' | 'unknown';
248
+ outputPath: string;
249
+ sampleCount?: number;
167
250
  }
@@ -41,6 +41,7 @@ export interface PyreConfig {
41
41
  watchdogMem?: number;
42
42
  webhookUrl?: string;
43
43
  alertCmd?: string;
44
+ dockerModeConfirmed?: boolean;
44
45
  visiblePanels?: {
45
46
  cpu?: boolean;
46
47
  mem?: boolean;
@@ -53,6 +54,8 @@ export interface PyreConfig {
53
54
  tasks?: boolean;
54
55
  disk?: boolean;
55
56
  process?: boolean;
57
+ containers?: boolean;
58
+ blender?: boolean;
56
59
  tree?: boolean;
57
60
  };
58
61
  panelLayout?: string[];
@@ -83,6 +86,7 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
83
86
  splashAnimation: 'classic',
84
87
  webhookUrl: '',
85
88
  alertCmd: '',
89
+ dockerModeConfirmed: false,
86
90
  visiblePanels: {
87
91
  cpu: true,
88
92
  mem: true,
@@ -95,6 +99,8 @@ export const DEFAULT_CONFIG: Required<PyreConfig> = {
95
99
  tasks: true,
96
100
  disk: true,
97
101
  process: true,
102
+ containers: true,
103
+ blender: true,
98
104
  tree: false,
99
105
  },
100
106
  panelLayout: ['mem', 'disk', 'net'],
File without changes