@axium/sysadmin 0.1.1 → 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.
@@ -1,3 +1,4 @@
1
+ import { execFileSync } from 'node:child_process';
1
2
  import * as fs from 'node:fs';
2
3
  import * as os from 'node:os';
3
4
  /** Read a sysfs/procfs file, returning the trimmed contents or undefined if unreadable. */
@@ -140,15 +141,26 @@ function networkInterfaces() {
140
141
  continue;
141
142
  const operstate = read(`/sys/class/net/${name}/operstate`);
142
143
  const connected = operstate === 'up' || read(`/sys/class/net/${name}/carrier`) === '1';
143
- // `speed` is in Mbit/s; only valid for connected links and may be -1/unreadable.
144
- const raw = read(`/sys/class/net/${name}/speed`);
145
- const speed = connected && raw && Number(raw) > 0 ? BigInt(raw) : undefined;
146
- // Wi-Fi devices expose a `wireless`/`phy80211` subdirectory.
144
+ const raw = Number(read(`/sys/class/net/${name}/speed`));
145
+ let speed = connected && Number.isSafeInteger(raw) && raw > 0 ? raw : undefined;
147
146
  const wireless = fs.existsSync(`/sys/class/net/${name}/wireless`) || fs.existsSync(`/sys/class/net/${name}/phy80211`);
147
+ // Wi-Fi doesn't expose sysfs `speed`; the SSID and link bitrate come from `iw` instead.
148
+ let connection;
149
+ if (wireless && connected)
150
+ try {
151
+ const link = execFileSync('iw', ['dev', name, 'link'], { encoding: 'utf8' });
152
+ connection = /^\s*SSID:\s*(.+)$/m.exec(link)?.[1].trim() || undefined;
153
+ const bitrate = /^\s*tx bitrate:\s*([\d.]+)\s*MBit\/s/m.exec(link)?.[1];
154
+ if (bitrate)
155
+ speed = Math.round(Number(bitrate));
156
+ }
157
+ catch {
158
+ // that's okay
159
+ }
148
160
  const uevent = read(`/sys/class/net/${name}/device/uevent`);
149
161
  const id = uevent && /^PCI_ID=(.+)$/m.exec(uevent)?.[1];
150
162
  const model = id ? pciName(id) : name;
151
- result.push({ name, model, connected, wireless, speed });
163
+ result.push({ name, model, connected, wireless, connection, speed });
152
164
  }
153
165
  return result;
154
166
  }
package/dist/common.d.ts CHANGED
@@ -297,7 +297,7 @@ declare const SysadminClientToServer: {
297
297
  connected: z.ZodBoolean;
298
298
  wireless: z.ZodBoolean;
299
299
  connection: z.ZodOptional<z.ZodString>;
300
- speed: z.ZodOptional<z.ZodCoercedBigInt<unknown>>;
300
+ speed: z.ZodOptional<z.ZodNumber>;
301
301
  }, z.core.$strip>>;
302
302
  arch: z.ZodString;
303
303
  machine: z.ZodString;
@@ -346,7 +346,7 @@ declare const SysadminServerToClient: {
346
346
  connected: z.ZodBoolean;
347
347
  wireless: z.ZodBoolean;
348
348
  connection: z.ZodOptional<z.ZodString>;
349
- speed: z.ZodOptional<z.ZodCoercedBigInt<unknown>>;
349
+ speed: z.ZodOptional<z.ZodNumber>;
350
350
  }, z.core.$strip>>;
351
351
  arch: z.ZodString;
352
352
  machine: z.ZodString;
package/dist/info.d.ts CHANGED
@@ -44,7 +44,7 @@ export declare const NetworkInterface: z.ZodObject<{
44
44
  connected: z.ZodBoolean;
45
45
  wireless: z.ZodBoolean;
46
46
  connection: z.ZodOptional<z.ZodString>;
47
- speed: z.ZodOptional<z.ZodCoercedBigInt<unknown>>;
47
+ speed: z.ZodOptional<z.ZodNumber>;
48
48
  }, z.core.$strip>;
49
49
  export interface NetworkInterface extends z.infer<typeof NetworkInterface> {
50
50
  }
@@ -80,7 +80,7 @@ export declare const SystemInfo: z.ZodObject<{
80
80
  connected: z.ZodBoolean;
81
81
  wireless: z.ZodBoolean;
82
82
  connection: z.ZodOptional<z.ZodString>;
83
- speed: z.ZodOptional<z.ZodCoercedBigInt<unknown>>;
83
+ speed: z.ZodOptional<z.ZodNumber>;
84
84
  }, z.core.$strip>>;
85
85
  arch: z.ZodString;
86
86
  machine: z.ZodString;
package/dist/info.js CHANGED
@@ -30,7 +30,7 @@ export const NetworkInterface = z.object({
30
30
  connected: z.boolean(),
31
31
  wireless: z.boolean(),
32
32
  connection: z.string().optional(),
33
- speed: z.coerce.bigint().optional(),
33
+ speed: z.number().nonnegative().optional(),
34
34
  });
35
35
  export const SystemInfo = z.object({
36
36
  cpus: CPU.array(),
package/locales/en.json CHANGED
@@ -67,6 +67,7 @@
67
67
  "wired": "Wired",
68
68
  "connected": "Connected",
69
69
  "disconnected": "Disconnected",
70
+ "connected_to": "to {connection}",
70
71
  "os": "Operating System",
71
72
  "platform": "Platform",
72
73
  "release": "Release",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/sysadmin",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "description": "System administration for Axium",
6
6
  "funding": {
@@ -23,6 +23,12 @@
23
23
  return text('sysadmin.system.usage', { used: formatBytes(used), total: formatBytes(total) });
24
24
  }
25
25
 
26
+ function speedText(speed: number): string {
27
+ if (speed < 1000) return `${speed} MBit/s`;
28
+ const gb = speed / 1000;
29
+ return `${gb % 1 === 0 ? gb : gb.toFixed(1)} GBit/s`;
30
+ }
31
+
26
32
  const socket = await connect();
27
33
 
28
34
  async function loadInfo() {
@@ -139,18 +145,23 @@
139
145
  <div class="component">
140
146
  <h3><Icon i="network-wired" /> {text('sysadmin.system.network')}</h3>
141
147
  {#each info.networkInterfaces as iface}
142
- <div class="line">
143
- <span class="icon-text">
148
+ <div class="net-line">
149
+ <span class="icon-text net-name">
144
150
  <Icon i={iface.wireless ? 'wifi' : 'ethernet'} />
145
151
  {iface.name}
146
152
  </span>
147
- <span class="subtle">{iface.model}</span>
148
- <span class={['net-status', iface.connected ? 'online' : 'offline']}>
149
- {iface.connected ? text('sysadmin.system.connected') : text('sysadmin.system.disconnected')}
153
+ <span class="subtle net-model">{iface.model}</span>
154
+ <span class="net-state">
155
+ <span class={['net-status', iface.connected ? 'online' : 'offline']}>
156
+ {iface.connected ? text('sysadmin.system.connected') : text('sysadmin.system.disconnected')}
157
+ {#if iface.connection}
158
+ <span class="subtle">{text('sysadmin.system.connected_to', { connection: iface.connection })}</span>
159
+ {/if}
160
+ </span>
161
+ {#if iface.speed}
162
+ <span class="subtle">{speedText(iface.speed)}</span>
163
+ {/if}
150
164
  </span>
151
- {#if iface.speed}
152
- <span>{iface.speed} Mbit/s</span>
153
- {/if}
154
165
  </div>
155
166
  {/each}
156
167
  </div>
@@ -266,6 +277,29 @@
266
277
  }
267
278
  }
268
279
 
280
+ .net-line {
281
+ display: flex;
282
+ align-items: center;
283
+ gap: 0.5em 1em;
284
+ flex-wrap: wrap;
285
+ }
286
+
287
+ .net-name {
288
+ font-weight: bold;
289
+ }
290
+
291
+ /* Push the status/speed cluster to the trailing edge; let the model take the slack. */
292
+ .net-model {
293
+ margin-right: auto;
294
+ }
295
+
296
+ .net-state {
297
+ display: inline-flex;
298
+ align-items: center;
299
+ gap: 0.35em 0.75em;
300
+ flex-wrap: wrap;
301
+ }
302
+
269
303
  dl {
270
304
  display: grid;
271
305
  grid-template-columns: max-content 1fr;
@@ -286,5 +320,9 @@
286
320
  padding: 1em;
287
321
  padding-bottom: 5em;
288
322
  }
323
+
324
+ .net-model {
325
+ flex-basis: 100%;
326
+ }
289
327
  }
290
328
  </style>