@axium/sysadmin 0.2.5 → 0.3.0
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/dist/client/cli.js +15 -2
- package/dist/client/info.js +154 -31
- package/dist/common.d.ts +43 -10
- package/dist/info.d.ts +52 -6
- package/dist/info.js +32 -3
- package/lib/SystemCard.svelte +1 -1
- package/lib/UserCard.svelte +1 -1
- package/lib/tsconfig.json +1 -1
- package/locales/en.json +2 -0
- package/package.json +5 -4
- package/routes/sysadmin/systems/[id]/+page.svelte +109 -5
- package/routes/tsconfig.json +1 -1
- package/dist/web_hook.d.ts +0 -8
- package/dist/web_hook.js +0 -4
package/dist/client/cli.js
CHANGED
|
@@ -9,6 +9,9 @@ import './socket.js';
|
|
|
9
9
|
function usage(info) {
|
|
10
10
|
return styleText('blueBright', formatBytes(info.used)) + '/' + styleText('blueBright', formatBytes(info.total));
|
|
11
11
|
}
|
|
12
|
+
function drive(device) {
|
|
13
|
+
return styleText('blueBright', formatBytes(device.size)) + (device.interface ? ' ' + styleText('dim', device.interface) : '');
|
|
14
|
+
}
|
|
12
15
|
const num = (value) => value === undefined ? styleText('red', '<unknown>') : styleText('blueBright', value.toString());
|
|
13
16
|
const tab = ' ', tab2 = ' ';
|
|
14
17
|
function dumpInfo(system, info) {
|
|
@@ -26,9 +29,19 @@ function dumpInfo(system, info) {
|
|
|
26
29
|
console.log('Memory:', usage(memory));
|
|
27
30
|
if (memory.swap)
|
|
28
31
|
console.log(tab, 'Swap:', usage(memory.swap));
|
|
32
|
+
const models = new Map(storage.devices.map(device => [device.name, device]));
|
|
29
33
|
console.log('Storage:');
|
|
30
|
-
for (const
|
|
31
|
-
|
|
34
|
+
for (const volume of storage.volumes) {
|
|
35
|
+
const labels = [volume.filesystem, ...(volume.profile ? [volume.profile.toUpperCase()] : [])];
|
|
36
|
+
console.log(tab, styleText('cyanBright', volume.mountPoints.join(' ')), styleText('dim', labels.join(' ')), usage(volume));
|
|
37
|
+
for (const name of volume.devices) {
|
|
38
|
+
const device = models.get(name);
|
|
39
|
+
console.log(tab2, styleText('yellow', device?.model ?? name), device ? drive(device) : '');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const device of storage.devices.filter(d => storage.volumes.every(v => !v.devices.includes(d.name)))) {
|
|
43
|
+
console.log(tab, styleText('yellow', device.model), drive(device), styleText('dim', '(unused)'));
|
|
44
|
+
}
|
|
32
45
|
console.log('Network:');
|
|
33
46
|
for (const iface of networkInterfaces) {
|
|
34
47
|
console.log(tab, styleText('cyanBright', iface.name), styleText('yellow', iface.model), iface.wireless ? '(wireless)' : '(wired)');
|
package/dist/client/info.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
4
5
|
/** Read a sysfs/procfs file, returning the trimmed contents or undefined if unreadable. */
|
|
5
6
|
function read(path) {
|
|
6
7
|
try {
|
|
@@ -64,16 +65,148 @@ function gpus() {
|
|
|
64
65
|
}
|
|
65
66
|
return result;
|
|
66
67
|
}
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
/** PCIe generations by their per-lane transfer rate in GT/s, as reported by `current_link_speed`. */
|
|
69
|
+
const pcieGenerations = {
|
|
70
|
+
'2.5': '1.0',
|
|
71
|
+
'5.0': '2.0',
|
|
72
|
+
'8.0': '3.0',
|
|
73
|
+
'16.0': '4.0',
|
|
74
|
+
'32.0': '5.0',
|
|
75
|
+
'64.0': '6.0',
|
|
76
|
+
'128.0': '7.0',
|
|
77
|
+
};
|
|
78
|
+
/** Format a link rate given in Mbit/s, e.g. 10000 -> '10 Gbps'. */
|
|
79
|
+
function formatLinkRate(mbps) {
|
|
80
|
+
return mbps >= 1000 ? `${(mbps / 1000).toFixed(1).replace(/\.0$/, '')} Gbps` : `${mbps} Mbps`;
|
|
81
|
+
}
|
|
82
|
+
/** The SATA link speed of an `ataN` device directory, e.g. '6.0 Gbps'. */
|
|
83
|
+
function sataSpeed(ata) {
|
|
84
|
+
for (const link of list(ata)) {
|
|
85
|
+
if (!link.startsWith('link'))
|
|
86
|
+
continue;
|
|
87
|
+
for (const classLink of list(`${ata}/${link}/ata_link`)) {
|
|
88
|
+
const speed = read(`${ata}/${link}/ata_link/${classLink}/sata_spd`);
|
|
89
|
+
// Reported as `<unknown>` for PATA links and for ports with nothing negotiated.
|
|
90
|
+
if (speed && !speed.startsWith('<'))
|
|
91
|
+
return speed;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** How a disk is attached, e.g. 'PCIe 4.0 x4', 'SATA 6.0 Gbps' or 'USB 10 Gbps'. */
|
|
96
|
+
function diskInterface(dev) {
|
|
97
|
+
let dir;
|
|
98
|
+
try {
|
|
99
|
+
dir = fs.realpathSync(`/sys/block/${dev}/device`);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
// Walk toward the root of the device tree; the nearest bus we recognize is the one the disk hangs off.
|
|
105
|
+
for (; dir.startsWith('/sys/devices/'); dir = path.dirname(dir)) {
|
|
106
|
+
const name = path.basename(dir);
|
|
107
|
+
if (/^ata\d+$/.test(name)) {
|
|
108
|
+
const speed = sataSpeed(dir);
|
|
109
|
+
return speed ? `SATA ${speed}` : 'SATA';
|
|
110
|
+
}
|
|
111
|
+
// PCI(e) endpoints expose the negotiated link; `current_link_speed` is like `16.0 GT/s PCIe`.
|
|
112
|
+
const link = read(`${dir}/current_link_speed`);
|
|
113
|
+
if (link) {
|
|
114
|
+
const rate = link.split(' ')[0];
|
|
115
|
+
const generation = pcieGenerations[Number(rate).toFixed(1)];
|
|
116
|
+
const width = read(`${dir}/current_link_width`);
|
|
117
|
+
return `PCIe ${generation ?? `${rate} GT/s`}${width && width !== '0' ? ` x${width}` : ''}`;
|
|
118
|
+
}
|
|
119
|
+
// USB devices (as opposed to their interfaces) carry the descriptor fields; `speed` is in Mbit/s.
|
|
120
|
+
if (fs.existsSync(`${dir}/idVendor`)) {
|
|
121
|
+
const speed = Number(read(`${dir}/speed`));
|
|
122
|
+
return speed > 0 ? `USB ${formatLinkRate(speed)}` : 'USB';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function devices() {
|
|
127
|
+
const result = [];
|
|
128
|
+
for (const name of list('/sys/block')) {
|
|
129
|
+
// Skip virtual devices (zram, loop, device-mapper, MD) which have no backing `device`.
|
|
130
|
+
if (!fs.existsSync(`/sys/block/${name}/device`))
|
|
131
|
+
continue;
|
|
132
|
+
const sectors = read(`/sys/block/${name}/size`);
|
|
133
|
+
if (!sectors)
|
|
134
|
+
continue;
|
|
135
|
+
result.push({
|
|
136
|
+
name,
|
|
137
|
+
model: read(`/sys/block/${name}/device/model`)?.trim() || name,
|
|
138
|
+
size: BigInt(sectors) * 512n,
|
|
139
|
+
interface: diskInterface(name),
|
|
140
|
+
rotational: read(`/sys/block/${name}/queue/rotational`) === '1',
|
|
141
|
+
removable: read(`/sys/block/${name}/removable`) === '1',
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Resolve a block device to the physical disks backing it, recursing through `slaves` so
|
|
148
|
+
* device-mapper (LVM, LUKS) and MD stacks resolve to real hardware rather than virtual devices.
|
|
149
|
+
*/
|
|
150
|
+
function physicalDisks(dev, into = new Set()) {
|
|
151
|
+
if (fs.existsSync(`/sys/class/block/${dev}/partition`)) {
|
|
152
|
+
// Partitions live inside their disk's directory: `.../nvme0n1/nvme0n1p3`.
|
|
153
|
+
try {
|
|
154
|
+
return physicalDisks(path.basename(path.dirname(fs.realpathSync(`/sys/class/block/${dev}`))), into);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return into;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (fs.existsSync(`/sys/block/${dev}/device`))
|
|
161
|
+
into.add(dev);
|
|
162
|
+
else
|
|
163
|
+
for (const slave of list(`/sys/block/${dev}/slaves`))
|
|
164
|
+
physicalDisks(slave, into);
|
|
165
|
+
return into;
|
|
166
|
+
}
|
|
167
|
+
/** BTRFS filesystems keyed by each of their member devices. The kernel is the only source that knows all members. */
|
|
168
|
+
function btrfsFilesystems() {
|
|
169
|
+
const result = new Map();
|
|
170
|
+
for (const fsid of list('/sys/fs/btrfs')) {
|
|
171
|
+
const devices = list(`/sys/fs/btrfs/${fsid}/devices`);
|
|
172
|
+
if (!devices.length)
|
|
173
|
+
continue;
|
|
174
|
+
// `allocation/data` holds a directory per chunk profile in use alongside its counter files.
|
|
175
|
+
const profile = list(`/sys/fs/btrfs/${fsid}/allocation/data`).find(entry => /^(single|dup|raid\d+(c\d+)?)$/.test(entry));
|
|
176
|
+
const info = { fsid, devices, profile: profile === 'single' ? undefined : profile };
|
|
177
|
+
for (const dev of devices)
|
|
178
|
+
result.set(dev, info);
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
function volumes() {
|
|
70
183
|
const mounts = read('/proc/mounts');
|
|
71
184
|
if (!mounts)
|
|
72
|
-
return
|
|
185
|
+
return [];
|
|
186
|
+
const btrfs = btrfsFilesystems();
|
|
187
|
+
// Several mount points can share one filesystem (BTRFS subvolumes, bind mounts); they are one volume.
|
|
188
|
+
const byFilesystem = new Map();
|
|
73
189
|
for (const line of mounts.split('\n')) {
|
|
74
|
-
const [source,
|
|
190
|
+
const [source, rawMountPoint, filesystem] = line.split(' ');
|
|
75
191
|
if (!source?.startsWith('/dev/'))
|
|
76
192
|
continue;
|
|
193
|
+
// /proc/mounts octal-escapes characters that would otherwise break the field separators.
|
|
194
|
+
const mountPoint = rawMountPoint.replace(/\\(\d{3})/g, (_, code) => String.fromCharCode(parseInt(code, 8)));
|
|
195
|
+
let dev;
|
|
196
|
+
try {
|
|
197
|
+
// The source may be a symlink, e.g. /dev/mapper/foo -> /dev/dm-0.
|
|
198
|
+
dev = fs.realpathSync(source).slice('/dev/'.length);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const btrfsInfo = btrfs.get(dev);
|
|
204
|
+
const existing = byFilesystem.get(btrfsInfo?.fsid ?? dev);
|
|
205
|
+
if (existing) {
|
|
206
|
+
if (!existing.mountPoints.includes(mountPoint))
|
|
207
|
+
existing.mountPoints.push(mountPoint);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
77
210
|
let stat;
|
|
78
211
|
try {
|
|
79
212
|
stat = fs.statfsSync(mountPoint, { bigint: true });
|
|
@@ -81,35 +214,25 @@ function mountUsage() {
|
|
|
81
214
|
catch {
|
|
82
215
|
continue;
|
|
83
216
|
}
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
217
|
+
// BTRFS tracks its own members; anything else spanning devices does so through MD or device-mapper.
|
|
218
|
+
const devices = new Set();
|
|
219
|
+
for (const member of btrfsInfo?.devices ?? [dev])
|
|
220
|
+
physicalDisks(member, devices);
|
|
221
|
+
if (!devices.size)
|
|
222
|
+
continue;
|
|
223
|
+
byFilesystem.set(btrfsInfo?.fsid ?? dev, {
|
|
224
|
+
mountPoints: [mountPoint],
|
|
225
|
+
filesystem,
|
|
226
|
+
devices: [...devices].sort((a, b) => a.localeCompare(b)),
|
|
227
|
+
profile: btrfsInfo?.profile ?? read(`/sys/block/${dev}/md/level`),
|
|
228
|
+
total: stat.blocks * stat.bsize,
|
|
229
|
+
used: (stat.blocks - stat.bfree) * stat.bsize,
|
|
230
|
+
});
|
|
87
231
|
}
|
|
88
|
-
return
|
|
232
|
+
return [...byFilesystem.values()].sort((a, b) => a.mountPoints[0].localeCompare(b.mountPoints[0]));
|
|
89
233
|
}
|
|
90
234
|
function storage() {
|
|
91
|
-
|
|
92
|
-
const result = [];
|
|
93
|
-
for (const dev of list('/sys/block')) {
|
|
94
|
-
// Skip virtual devices (zram, loop, device-mapper) which have no backing `device`.
|
|
95
|
-
if (!fs.existsSync(`/sys/block/${dev}/device`))
|
|
96
|
-
continue;
|
|
97
|
-
const sectors = read(`/sys/block/${dev}/size`);
|
|
98
|
-
if (!sectors)
|
|
99
|
-
continue;
|
|
100
|
-
// `size` is always in 512-byte sectors regardless of logical block size.
|
|
101
|
-
const total = BigInt(sectors) * 512n;
|
|
102
|
-
// Sum filesystem usage across mounted partitions of this disk.
|
|
103
|
-
let used = 0n;
|
|
104
|
-
for (const [mounted, info] of usage) {
|
|
105
|
-
if (mounted === dev || mounted.startsWith(dev + 'p') || (mounted.startsWith(dev) && /\d$/.test(mounted))) {
|
|
106
|
-
used += info.used;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const model = read(`/sys/block/${dev}/device/model`)?.trim() || dev;
|
|
110
|
-
result.push({ model, total, used });
|
|
111
|
-
}
|
|
112
|
-
return result;
|
|
235
|
+
return { devices: devices(), volumes: volumes() };
|
|
113
236
|
}
|
|
114
237
|
let dmiMemory;
|
|
115
238
|
/** Static memory hardware details from DMI. */
|
package/dist/common.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export declare const System: z.ZodObject<{
|
|
|
31
31
|
emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
|
|
32
32
|
preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
|
|
33
33
|
debug: z.ZodDefault<z.ZodBoolean>;
|
|
34
|
+
theme: z.ZodDefault<z.ZodLiteral<"default" | "light" | "forest" | "midnight" | "beach" | "cherry" | "volcano">>;
|
|
34
35
|
}, z.core.$strip>>>;
|
|
35
36
|
roles: z.ZodArray<z.ZodString>;
|
|
36
37
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -86,6 +87,7 @@ declare const SysadminAPI: {
|
|
|
86
87
|
emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
|
|
87
88
|
preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
|
|
88
89
|
debug: z.ZodDefault<z.ZodBoolean>;
|
|
90
|
+
theme: z.ZodDefault<z.ZodLiteral<"default" | "light" | "forest" | "midnight" | "beach" | "cherry" | "volcano">>;
|
|
89
91
|
}, z.core.$strip>>>;
|
|
90
92
|
roles: z.ZodArray<z.ZodString>;
|
|
91
93
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -122,6 +124,7 @@ declare const SysadminAPI: {
|
|
|
122
124
|
emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
|
|
123
125
|
preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
|
|
124
126
|
debug: z.ZodDefault<z.ZodBoolean>;
|
|
127
|
+
theme: z.ZodDefault<z.ZodLiteral<"default" | "light" | "forest" | "midnight" | "beach" | "cherry" | "volcano">>;
|
|
125
128
|
}, z.core.$strip>>>;
|
|
126
129
|
roles: z.ZodArray<z.ZodString>;
|
|
127
130
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -172,6 +175,7 @@ declare const SysadminAPI: {
|
|
|
172
175
|
emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
|
|
173
176
|
preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
|
|
174
177
|
debug: z.ZodDefault<z.ZodBoolean>;
|
|
178
|
+
theme: z.ZodDefault<z.ZodLiteral<"default" | "light" | "forest" | "midnight" | "beach" | "cherry" | "volcano">>;
|
|
175
179
|
}, z.core.$strip>>>;
|
|
176
180
|
roles: z.ZodArray<z.ZodString>;
|
|
177
181
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -208,6 +212,7 @@ declare const SysadminAPI: {
|
|
|
208
212
|
emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
|
|
209
213
|
preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
|
|
210
214
|
debug: z.ZodDefault<z.ZodBoolean>;
|
|
215
|
+
theme: z.ZodDefault<z.ZodLiteral<"default" | "light" | "forest" | "midnight" | "beach" | "cherry" | "volcano">>;
|
|
211
216
|
}, z.core.$strip>>>;
|
|
212
217
|
roles: z.ZodArray<z.ZodString>;
|
|
213
218
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -239,6 +244,7 @@ declare const SysadminAPI: {
|
|
|
239
244
|
emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
|
|
240
245
|
preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
|
|
241
246
|
debug: z.ZodDefault<z.ZodBoolean>;
|
|
247
|
+
theme: z.ZodDefault<z.ZodLiteral<"default" | "light" | "forest" | "midnight" | "beach" | "cherry" | "volcano">>;
|
|
242
248
|
}, z.core.$strip>>>;
|
|
243
249
|
roles: z.ZodArray<z.ZodString>;
|
|
244
250
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -295,6 +301,7 @@ declare const SysadminAPI: {
|
|
|
295
301
|
emailVerified: z.ZodOptional<z.ZodOptional<z.ZodNullable<z.ZodCoercedDate<unknown>>>>;
|
|
296
302
|
preferences: z.ZodOptional<z.ZodLazy<z.ZodObject<{
|
|
297
303
|
debug: z.ZodDefault<z.ZodBoolean>;
|
|
304
|
+
theme: z.ZodDefault<z.ZodLiteral<"default" | "light" | "forest" | "midnight" | "beach" | "cherry" | "volcano">>;
|
|
298
305
|
}, z.core.$strip>>>;
|
|
299
306
|
roles: z.ZodArray<z.ZodString>;
|
|
300
307
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -336,11 +343,24 @@ declare const SysadminClientToServer: {
|
|
|
336
343
|
total: z.ZodCoercedBigInt<unknown>;
|
|
337
344
|
used: z.ZodCoercedBigInt<unknown>;
|
|
338
345
|
}, z.core.$strip>;
|
|
339
|
-
storage: z.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
346
|
+
storage: z.ZodObject<{
|
|
347
|
+
devices: z.ZodArray<z.ZodObject<{
|
|
348
|
+
name: z.ZodString;
|
|
349
|
+
model: z.ZodString;
|
|
350
|
+
size: z.ZodCoercedBigInt<unknown>;
|
|
351
|
+
interface: z.ZodOptional<z.ZodString>;
|
|
352
|
+
rotational: z.ZodBoolean;
|
|
353
|
+
removable: z.ZodBoolean;
|
|
354
|
+
}, z.core.$strip>>;
|
|
355
|
+
volumes: z.ZodArray<z.ZodObject<{
|
|
356
|
+
mountPoints: z.ZodArray<z.ZodString>;
|
|
357
|
+
filesystem: z.ZodString;
|
|
358
|
+
devices: z.ZodArray<z.ZodString>;
|
|
359
|
+
profile: z.ZodOptional<z.ZodString>;
|
|
360
|
+
total: z.ZodCoercedBigInt<unknown>;
|
|
361
|
+
used: z.ZodCoercedBigInt<unknown>;
|
|
362
|
+
}, z.core.$strip>>;
|
|
363
|
+
}, z.core.$strip>;
|
|
344
364
|
networkInterfaces: z.ZodArray<z.ZodObject<{
|
|
345
365
|
name: z.ZodString;
|
|
346
366
|
model: z.ZodString;
|
|
@@ -394,11 +414,24 @@ declare const SysadminServerToClient: {
|
|
|
394
414
|
total: z.ZodCoercedBigInt<unknown>;
|
|
395
415
|
used: z.ZodCoercedBigInt<unknown>;
|
|
396
416
|
}, z.core.$strip>;
|
|
397
|
-
storage: z.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
417
|
+
storage: z.ZodObject<{
|
|
418
|
+
devices: z.ZodArray<z.ZodObject<{
|
|
419
|
+
name: z.ZodString;
|
|
420
|
+
model: z.ZodString;
|
|
421
|
+
size: z.ZodCoercedBigInt<unknown>;
|
|
422
|
+
interface: z.ZodOptional<z.ZodString>;
|
|
423
|
+
rotational: z.ZodBoolean;
|
|
424
|
+
removable: z.ZodBoolean;
|
|
425
|
+
}, z.core.$strip>>;
|
|
426
|
+
volumes: z.ZodArray<z.ZodObject<{
|
|
427
|
+
mountPoints: z.ZodArray<z.ZodString>;
|
|
428
|
+
filesystem: z.ZodString;
|
|
429
|
+
devices: z.ZodArray<z.ZodString>;
|
|
430
|
+
profile: z.ZodOptional<z.ZodString>;
|
|
431
|
+
total: z.ZodCoercedBigInt<unknown>;
|
|
432
|
+
used: z.ZodCoercedBigInt<unknown>;
|
|
433
|
+
}, z.core.$strip>>;
|
|
434
|
+
}, z.core.$strip>;
|
|
402
435
|
networkInterfaces: z.ZodArray<z.ZodObject<{
|
|
403
436
|
name: z.ZodString;
|
|
404
437
|
model: z.ZodString;
|
package/dist/info.d.ts
CHANGED
|
@@ -34,11 +34,44 @@ export declare const Memory: z.ZodObject<{
|
|
|
34
34
|
}, z.core.$strip>;
|
|
35
35
|
export interface Memory extends z.infer<typeof Memory> {
|
|
36
36
|
}
|
|
37
|
-
export declare const
|
|
37
|
+
export declare const StorageDevice: z.ZodObject<{
|
|
38
|
+
name: z.ZodString;
|
|
38
39
|
model: z.ZodString;
|
|
40
|
+
size: z.ZodCoercedBigInt<unknown>;
|
|
41
|
+
interface: z.ZodOptional<z.ZodString>;
|
|
42
|
+
rotational: z.ZodBoolean;
|
|
43
|
+
removable: z.ZodBoolean;
|
|
44
|
+
}, z.core.$strip>;
|
|
45
|
+
export interface StorageDevice extends z.infer<typeof StorageDevice> {
|
|
46
|
+
}
|
|
47
|
+
export declare const StorageVolume: z.ZodObject<{
|
|
48
|
+
mountPoints: z.ZodArray<z.ZodString>;
|
|
49
|
+
filesystem: z.ZodString;
|
|
50
|
+
devices: z.ZodArray<z.ZodString>;
|
|
51
|
+
profile: z.ZodOptional<z.ZodString>;
|
|
39
52
|
total: z.ZodCoercedBigInt<unknown>;
|
|
40
53
|
used: z.ZodCoercedBigInt<unknown>;
|
|
41
54
|
}, z.core.$strip>;
|
|
55
|
+
export interface StorageVolume extends z.infer<typeof StorageVolume> {
|
|
56
|
+
}
|
|
57
|
+
export declare const Storage: z.ZodObject<{
|
|
58
|
+
devices: z.ZodArray<z.ZodObject<{
|
|
59
|
+
name: z.ZodString;
|
|
60
|
+
model: z.ZodString;
|
|
61
|
+
size: z.ZodCoercedBigInt<unknown>;
|
|
62
|
+
interface: z.ZodOptional<z.ZodString>;
|
|
63
|
+
rotational: z.ZodBoolean;
|
|
64
|
+
removable: z.ZodBoolean;
|
|
65
|
+
}, z.core.$strip>>;
|
|
66
|
+
volumes: z.ZodArray<z.ZodObject<{
|
|
67
|
+
mountPoints: z.ZodArray<z.ZodString>;
|
|
68
|
+
filesystem: z.ZodString;
|
|
69
|
+
devices: z.ZodArray<z.ZodString>;
|
|
70
|
+
profile: z.ZodOptional<z.ZodString>;
|
|
71
|
+
total: z.ZodCoercedBigInt<unknown>;
|
|
72
|
+
used: z.ZodCoercedBigInt<unknown>;
|
|
73
|
+
}, z.core.$strip>>;
|
|
74
|
+
}, z.core.$strip>;
|
|
42
75
|
export interface Storage extends z.infer<typeof Storage> {
|
|
43
76
|
}
|
|
44
77
|
export declare const NetworkInterface: z.ZodObject<{
|
|
@@ -83,11 +116,24 @@ export declare const SystemInfo: z.ZodObject<{
|
|
|
83
116
|
total: z.ZodCoercedBigInt<unknown>;
|
|
84
117
|
used: z.ZodCoercedBigInt<unknown>;
|
|
85
118
|
}, z.core.$strip>;
|
|
86
|
-
storage: z.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
119
|
+
storage: z.ZodObject<{
|
|
120
|
+
devices: z.ZodArray<z.ZodObject<{
|
|
121
|
+
name: z.ZodString;
|
|
122
|
+
model: z.ZodString;
|
|
123
|
+
size: z.ZodCoercedBigInt<unknown>;
|
|
124
|
+
interface: z.ZodOptional<z.ZodString>;
|
|
125
|
+
rotational: z.ZodBoolean;
|
|
126
|
+
removable: z.ZodBoolean;
|
|
127
|
+
}, z.core.$strip>>;
|
|
128
|
+
volumes: z.ZodArray<z.ZodObject<{
|
|
129
|
+
mountPoints: z.ZodArray<z.ZodString>;
|
|
130
|
+
filesystem: z.ZodString;
|
|
131
|
+
devices: z.ZodArray<z.ZodString>;
|
|
132
|
+
profile: z.ZodOptional<z.ZodString>;
|
|
133
|
+
total: z.ZodCoercedBigInt<unknown>;
|
|
134
|
+
used: z.ZodCoercedBigInt<unknown>;
|
|
135
|
+
}, z.core.$strip>>;
|
|
136
|
+
}, z.core.$strip>;
|
|
91
137
|
networkInterfaces: z.ZodArray<z.ZodObject<{
|
|
92
138
|
name: z.ZodString;
|
|
93
139
|
model: z.ZodString;
|
package/dist/info.js
CHANGED
|
@@ -24,9 +24,38 @@ export const Memory = z.object({
|
|
|
24
24
|
/** Only available when swap is in use */
|
|
25
25
|
swap: TotalUsed.optional(),
|
|
26
26
|
});
|
|
27
|
-
export const
|
|
28
|
-
|
|
27
|
+
export const StorageDevice = z.object({
|
|
28
|
+
/** Kernel name of the block device, e.g. 'nvme0n1' or 'sda' */
|
|
29
|
+
name: z.string(),
|
|
29
30
|
model: z.string(),
|
|
31
|
+
/** Capacity in bytes */
|
|
32
|
+
size: z.coerce.bigint(),
|
|
33
|
+
/** How the device is attached, e.g. 'PCIe 4.0 x4' or 'SATA 6.0 Gbps' */
|
|
34
|
+
interface: z.string().optional(),
|
|
35
|
+
/** Whether the device is a spinning disk rather than solid state */
|
|
36
|
+
rotational: z.boolean(),
|
|
37
|
+
/** Whether the device's media can be removed, e.g. a card reader or optical drive */
|
|
38
|
+
removable: z.boolean(),
|
|
39
|
+
});
|
|
40
|
+
export const StorageVolume = z.object({
|
|
41
|
+
...TotalUsed.shape,
|
|
42
|
+
/** Every mount point of the filesystem; more than one for e.g. BTRFS subvolumes or bind mounts */
|
|
43
|
+
mountPoints: z.string().array(),
|
|
44
|
+
/** Filesystem type, e.g. 'btrfs' or 'ext4' */
|
|
45
|
+
filesystem: z.string(),
|
|
46
|
+
/** Names of the devices backing this volume, matching `StorageDevice.name` */
|
|
47
|
+
devices: z.string().array(),
|
|
48
|
+
/**
|
|
49
|
+
* The RAID or allocation profile the volume is stored with, e.g. 'raid1'.
|
|
50
|
+
* Only known for filesystems that manage their own devices (BTRFS) and MD arrays; unset when it is plain 'single'.
|
|
51
|
+
*/
|
|
52
|
+
profile: z.string().optional(),
|
|
53
|
+
});
|
|
54
|
+
export const Storage = z.object({
|
|
55
|
+
/** Physical devices, including ones not backing any volume */
|
|
56
|
+
devices: StorageDevice.array(),
|
|
57
|
+
/** Mounted filesystems, each of which may span several devices */
|
|
58
|
+
volumes: StorageVolume.array(),
|
|
30
59
|
});
|
|
31
60
|
export const NetworkInterface = z.object({
|
|
32
61
|
name: z.string(),
|
|
@@ -47,7 +76,7 @@ export const SystemInfo = z.object({
|
|
|
47
76
|
cpus: CPU.array(),
|
|
48
77
|
gpus: GPU.array(),
|
|
49
78
|
memory: Memory,
|
|
50
|
-
storage: Storage
|
|
79
|
+
storage: Storage,
|
|
51
80
|
networkInterfaces: NetworkInterface.array(),
|
|
52
81
|
/** e.g. 'arm', 'arm64', 'ia32', 'loong64', 'mips', 'mipsel', 'ppc64', 'riscv64', 's390x', and 'x64' */
|
|
53
82
|
arch: z.string(),
|
package/lib/SystemCard.svelte
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { fetchAPI, text } from '@axium/client';
|
|
3
3
|
import { contextMenu, type ContextMenuItem } from '@axium/client/attachments';
|
|
4
4
|
import { Icon, Popover } from '@axium/client/components';
|
|
5
|
-
import { copy } from '@axium/client/
|
|
5
|
+
import { copy } from '@axium/client/web';
|
|
6
6
|
import { toastStatus } from '@axium/client/toast';
|
|
7
7
|
import { systemTypeIcons, type System } from '@axium/sysadmin';
|
|
8
8
|
import SystemInitDialog from './SystemInitDialog.svelte';
|
package/lib/UserCard.svelte
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { fetchAPI, text } from '@axium/client';
|
|
3
3
|
import { contextMenu, type ContextMenuItem } from '@axium/client/attachments';
|
|
4
4
|
import { Icon, Popover } from '@axium/client/components';
|
|
5
|
-
import { copy } from '@axium/client/
|
|
5
|
+
import { copy } from '@axium/client/web';
|
|
6
6
|
import { toastStatus } from '@axium/client/toast';
|
|
7
7
|
import type { SystemUser } from '@axium/sysadmin';
|
|
8
8
|
import UserInitDialog from './UserInitDialog.svelte';
|
package/lib/tsconfig.json
CHANGED
package/locales/en.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axium/sysadmin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"author": "James Prevett <axium@jamespre.dev>",
|
|
5
5
|
"description": "System administration for Axium",
|
|
6
6
|
"funding": {
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"build": "tsc"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
|
-
"@axium/client": ">=0.
|
|
40
|
-
"@axium/core": ">=0.
|
|
39
|
+
"@axium/client": ">=0.37.0",
|
|
40
|
+
"@axium/core": ">=0.39.0",
|
|
41
41
|
"@axium/server": ">=0.47.0",
|
|
42
42
|
"@sveltejs/kit": "^2.27.3",
|
|
43
43
|
"kysely": "^0.29.0",
|
|
@@ -49,11 +49,12 @@
|
|
|
49
49
|
"zod": "^4.0.5"
|
|
50
50
|
},
|
|
51
51
|
"axium": {
|
|
52
|
+
"locales": "locales",
|
|
52
53
|
"server": {
|
|
53
54
|
"routes": "routes",
|
|
54
55
|
"hooks": "./dist/server/hooks.js",
|
|
55
56
|
"db": "./db.json",
|
|
56
|
-
"web_client_hooks": "./dist/
|
|
57
|
+
"web_client_hooks": "./dist/common.js"
|
|
57
58
|
},
|
|
58
59
|
"client": {
|
|
59
60
|
"hooks": "./dist/client/hooks.js",
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
let loading = $state(true);
|
|
18
18
|
|
|
19
19
|
const matchingUser = $derived(info && systemUsers.find(u => u.username === info!.user.username));
|
|
20
|
+
|
|
21
|
+
const storageDevices = $derived(new Map((info?.storage.devices ?? []).map(device => [device.name, device])));
|
|
22
|
+
const unusedDevices = $derived(
|
|
23
|
+
(info?.storage.devices ?? []).filter(device => info!.storage.volumes.every(volume => !volume.devices.includes(device.name)))
|
|
24
|
+
);
|
|
20
25
|
const connectedUser = $derived(system.connectedUserId ? systemUsers.find(u => u.id === system.connectedUserId) : undefined);
|
|
21
26
|
|
|
22
27
|
async function setConnectedUser(connectedUserId: string | null) {
|
|
@@ -147,14 +152,57 @@
|
|
|
147
152
|
{/if}
|
|
148
153
|
</div>
|
|
149
154
|
|
|
150
|
-
{#if info.storage.length}
|
|
155
|
+
{#if info.storage.devices.length || info.storage.volumes.length}
|
|
151
156
|
<div class="component">
|
|
152
157
|
<h3><Icon i="hard-drive" /> {text('sysadmin.system.storage')}</h3>
|
|
153
|
-
{#each info.storage as
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
{#each info.storage.volumes as volume}
|
|
159
|
+
{const disks = $derived(volume.devices.map(name => storageDevices.get(name)).filter(disk => !!disk))}
|
|
160
|
+
<div class="storage-line">
|
|
161
|
+
<span class="mount">{volume.mountPoints.join(' ')}</span>
|
|
162
|
+
{#if disks.length === 1}
|
|
163
|
+
<span class="subtle">{disks[0].model}</span>
|
|
164
|
+
{/if}
|
|
165
|
+
<span class="tags">
|
|
166
|
+
<span class="tag">{volume.filesystem}</span>
|
|
167
|
+
{#if volume.profile}
|
|
168
|
+
<span class="tag raid">{volume.profile.toUpperCase()}</span>
|
|
169
|
+
{/if}
|
|
170
|
+
{#if disks.length > 1}
|
|
171
|
+
<span class="subtle">{text('sysadmin.system.storage_array', { count: disks.length })}</span>
|
|
172
|
+
{:else if disks[0]?.interface}
|
|
173
|
+
<span class="subtle">{disks[0].interface}</span>
|
|
174
|
+
{/if}
|
|
175
|
+
</span>
|
|
176
|
+
</div>
|
|
177
|
+
<NumberBar value={fraction(volume.used, volume.total)} max={1} text={usageText(volume.used, volume.total)} />
|
|
178
|
+
{#if disks.length > 1}
|
|
179
|
+
<div class="disks">
|
|
180
|
+
{#each disks as disk}
|
|
181
|
+
<div class="disk">
|
|
182
|
+
<span class="icon-text">
|
|
183
|
+
<Icon i={disk.rotational ? 'hard-drive' : 'memory'} />
|
|
184
|
+
{disk.model}
|
|
185
|
+
</span>
|
|
186
|
+
<span class="subtle">
|
|
187
|
+
{formatBytes(disk.size)}
|
|
188
|
+
{#if disk.interface}<span class="dot">·</span>{disk.interface}{/if}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
{/each}
|
|
192
|
+
</div>
|
|
193
|
+
{/if}
|
|
194
|
+
{/each}
|
|
195
|
+
{#each unusedDevices as disk}
|
|
196
|
+
<div class="storage-line">
|
|
197
|
+
<span class="mount">{disk.model}</span>
|
|
198
|
+
<span class="tags">
|
|
199
|
+
<span class="subtle">
|
|
200
|
+
{formatBytes(disk.size)}
|
|
201
|
+
{#if disk.interface}<span class="dot">·</span>{disk.interface}{/if}
|
|
202
|
+
</span>
|
|
203
|
+
<span class="tag unused">{text('sysadmin.system.storage_unused')}</span>
|
|
204
|
+
</span>
|
|
156
205
|
</div>
|
|
157
|
-
<NumberBar value={fraction(disk.used, disk.total)} max={1} text={usageText(disk.used, disk.total)} />
|
|
158
206
|
{/each}
|
|
159
207
|
</div>
|
|
160
208
|
{/if}
|
|
@@ -361,6 +409,62 @@
|
|
|
361
409
|
}
|
|
362
410
|
}
|
|
363
411
|
|
|
412
|
+
.storage-line {
|
|
413
|
+
display: flex;
|
|
414
|
+
align-items: center;
|
|
415
|
+
gap: 0.35em 1em;
|
|
416
|
+
flex-wrap: wrap;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.mount {
|
|
420
|
+
font-weight: bold;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.tags {
|
|
424
|
+
margin-left: auto;
|
|
425
|
+
display: inline-flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
gap: 0.5em;
|
|
428
|
+
flex-wrap: wrap;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.tag {
|
|
432
|
+
padding: 0.1em 0.6em;
|
|
433
|
+
border-radius: 1em;
|
|
434
|
+
font-size: 0.85em;
|
|
435
|
+
line-height: 1.5;
|
|
436
|
+
background-color: hsl(0 0 calc(var(--bg-light) + (var(--light-step) * 2)));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.raid {
|
|
440
|
+
background-color: var(--bg-strong);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.unused {
|
|
444
|
+
color: hsl(0 0 var(--fg-light));
|
|
445
|
+
background-color: transparent;
|
|
446
|
+
border: 1px solid hsl(0 0 calc(var(--bg-light) + (var(--light-step) * 3)));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.disks {
|
|
450
|
+
display: grid;
|
|
451
|
+
grid-template-columns: repeat(auto-fit, minmax(15em, 1fr));
|
|
452
|
+
gap: 0.5em;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.disk {
|
|
456
|
+
display: flex;
|
|
457
|
+
flex-direction: column;
|
|
458
|
+
gap: 0.15em;
|
|
459
|
+
padding: 0.5em 0.75em;
|
|
460
|
+
border-radius: 0.5em;
|
|
461
|
+
background-color: hsl(0 0 calc(var(--bg-light) + var(--light-step)));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.dot {
|
|
465
|
+
margin: 0 0.15em;
|
|
466
|
+
}
|
|
467
|
+
|
|
364
468
|
.net-line {
|
|
365
469
|
display: flex;
|
|
366
470
|
align-items: center;
|
package/routes/tsconfig.json
CHANGED
package/dist/web_hook.d.ts
DELETED
package/dist/web_hook.js
DELETED