@axium/sysadmin 0.0.1

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 (50) hide show
  1. package/LICENSE.md +157 -0
  2. package/README.md +3 -0
  3. package/db.json +77 -0
  4. package/dist/_probe.d.ts +1 -0
  5. package/dist/_probe.js +4 -0
  6. package/dist/client/.info.js +38 -0
  7. package/dist/client/cli.d.ts +1 -0
  8. package/dist/client/cli.js +109 -0
  9. package/dist/client/hooks.d.ts +1 -0
  10. package/dist/client/hooks.js +1 -0
  11. package/dist/client/info.d.ts +2 -0
  12. package/dist/client/info.js +171 -0
  13. package/dist/client/socket.d.ts +1 -0
  14. package/dist/client/socket.js +9 -0
  15. package/dist/common.d.ts +371 -0
  16. package/dist/common.js +65 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +2 -0
  19. package/dist/info.d.ts +95 -0
  20. package/dist/info.js +58 -0
  21. package/dist/server/api.d.ts +1 -0
  22. package/dist/server/api.js +136 -0
  23. package/dist/server/db.d.ts +7 -0
  24. package/dist/server/db.js +1 -0
  25. package/dist/server/hooks.d.ts +3 -0
  26. package/dist/server/hooks.js +3 -0
  27. package/dist/server/socket.d.ts +1 -0
  28. package/dist/server/socket.js +9 -0
  29. package/dist/web_hook.d.ts +8 -0
  30. package/dist/web_hook.js +4 -0
  31. package/lib/SystemCard.svelte +133 -0
  32. package/lib/SystemInitDialog.svelte +59 -0
  33. package/lib/UserCard.svelte +108 -0
  34. package/lib/UserInitDialog.svelte +49 -0
  35. package/lib/index.ts +4 -0
  36. package/lib/tsconfig.json +12 -0
  37. package/locales/en.json +79 -0
  38. package/package.json +71 -0
  39. package/routes/sysadmin/+layout.ts +22 -0
  40. package/routes/sysadmin/+page.svelte +115 -0
  41. package/routes/sysadmin/+page.ts +14 -0
  42. package/routes/sysadmin/systems/+page.svelte +92 -0
  43. package/routes/sysadmin/systems/+page.ts +11 -0
  44. package/routes/sysadmin/systems/[id]/+page.svelte +290 -0
  45. package/routes/sysadmin/systems/[id]/+page.ts +11 -0
  46. package/routes/sysadmin/users/+page.svelte +85 -0
  47. package/routes/sysadmin/users/+page.ts +11 -0
  48. package/routes/sysadmin/users/[id]/+page.svelte +82 -0
  49. package/routes/sysadmin/users/[id]/+page.ts +11 -0
  50. package/routes/tsconfig.json +12 -0
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@axium/sysadmin",
3
+ "version": "0.0.1",
4
+ "author": "James Prevett <axium@jamespre.dev>",
5
+ "description": "System administration for Axium",
6
+ "funding": {
7
+ "type": "individual",
8
+ "url": "https://github.com/sponsors/james-pre"
9
+ },
10
+ "license": "LGPL-3.0-or-later",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/james-pre/axium.git"
14
+ },
15
+ "homepage": "https://github.com/james-pre/axium#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/james-pre/axium/issues"
18
+ },
19
+ "type": "module",
20
+ "main": "dist/index.js",
21
+ "types": "dist/index.d.ts",
22
+ "exports": {
23
+ ".": "./dist/index.js",
24
+ "./*": "./dist/*.js",
25
+ "./components": "./lib/index.js",
26
+ "./components/*": "./lib/*.svelte"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "lib",
31
+ "routes",
32
+ "locales",
33
+ "db.json"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc"
37
+ },
38
+ "peerDependencies": {
39
+ "@axium/client": ">=0.27.0",
40
+ "@axium/core": ">=0.23.0",
41
+ "@axium/server": ">=0.47.0",
42
+ "@sveltejs/kit": "^2.27.3",
43
+ "kysely": "^0.29.0",
44
+ "utilium": "^3.0.0"
45
+ },
46
+ "dependencies": {
47
+ "commander": "^15.0.0",
48
+ "ioium": "^1.6.0",
49
+ "zod": "^4.0.5"
50
+ },
51
+ "axium": {
52
+ "server": {
53
+ "routes": "routes",
54
+ "hooks": "./dist/server/hooks.js",
55
+ "db": "./db.json",
56
+ "web_client_hooks": "./dist/web_hook.js"
57
+ },
58
+ "client": {
59
+ "hooks": "./dist/client/hooks.js",
60
+ "cli": "./dist/client/cli.js"
61
+ },
62
+ "apps": [
63
+ {
64
+ "id": "sysadmin",
65
+ "name": "System Administration",
66
+ "icon": "screwdriver-wrench"
67
+ }
68
+ ],
69
+ "update_checks": true
70
+ }
71
+ }
@@ -0,0 +1,22 @@
1
+ import { connect } from '@axium/client/socket';
2
+ import { getCurrentSession } from '@axium/client/user';
3
+ import type { Session, User } from '@axium/core';
4
+
5
+ export const ssr = false;
6
+
7
+ export async function load({ parent }) {
8
+ let { session }: { session?: (Session & { user: User }) | null } = await parent();
9
+
10
+ session ||= await getCurrentSession().catch(() => null);
11
+
12
+ if (!session) {
13
+ window.location.href = '/login?after=/sysadmin';
14
+ throw 'Missing session, redirecting to login';
15
+ }
16
+
17
+ await connect();
18
+
19
+ const { user } = session;
20
+
21
+ return { session, user };
22
+ }
@@ -0,0 +1,115 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { Icon } from '@axium/client/components';
4
+ import { socket } from '@axium/client/socket';
5
+ import { SystemCard, SystemInitDialog, UserCard, UserInitDialog } from '@axium/sysadmin/components';
6
+
7
+ const { data } = $props();
8
+
9
+ let systems = $state(data.systems);
10
+ let users = $state(data.users);
11
+
12
+ const onlineHosts = $derived(await socket?.emitWithAck('sysadmin:ping').then(systems => systems.map(s => s.hostname)));
13
+ </script>
14
+
15
+ <svelte:head>
16
+ <title>{text('sysadmin.page_title')}</title>
17
+ </svelte:head>
18
+
19
+ <div class="sysadmin-main">
20
+ <h1>{text('sysadmin.page_title')}</h1>
21
+
22
+ <section>
23
+ <div class="section-header">
24
+ <h2>{text('sysadmin.systems')}</h2>
25
+ <a class="subtle" href="/sysadmin/systems">{text('sysadmin.view_all')}</a>
26
+ <button class="icon-text" command="show-modal" commandfor="system-init">
27
+ <Icon i="plus" />
28
+ <span>{text('sysadmin.new_system')}</span>
29
+ </button>
30
+ </div>
31
+
32
+ {#if systems.length}
33
+ <div class="cards">
34
+ {#each systems as system, i (system.id)}
35
+ <SystemCard
36
+ bind:system={systems[i]}
37
+ online={!!onlineHosts?.includes(system.hostname)}
38
+ deleted={() => systems.splice(i, 1)}
39
+ />
40
+ {/each}
41
+ </div>
42
+ {:else}
43
+ <p class="subtle">{text('sysadmin.no_systems')}</p>
44
+ {/if}
45
+ </section>
46
+
47
+ <section>
48
+ <div class="section-header">
49
+ <h2>{text('sysadmin.system_users')}</h2>
50
+ <button class="icon-text" command="show-modal" commandfor="system-user-init">
51
+ <Icon i="plus" />
52
+ <span>{text('sysadmin.new_system_user')}</span>
53
+ </button>
54
+ </div>
55
+
56
+ {#if users.length}
57
+ <div class="cards">
58
+ {#each users as user, i (user.id)}
59
+ <UserCard bind:user={users[i]} deleted={() => users.splice(i, 1)} />
60
+ {/each}
61
+ </div>
62
+ {:else}
63
+ <p class="subtle">{text('sysadmin.no_system_users')}</p>
64
+ {/if}
65
+ </section>
66
+ </div>
67
+
68
+ <SystemInitDialog userId={data.session.userId} created={system => systems.push(system)} />
69
+ <UserInitDialog userId={data.session.userId} created={user => users.push(user)} />
70
+
71
+ <style>
72
+ .sysadmin-main {
73
+ padding: 2em;
74
+ display: flex;
75
+ flex-direction: column;
76
+ gap: 2em;
77
+ }
78
+
79
+ section {
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 1em;
83
+ }
84
+
85
+ .section-header {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 1em;
89
+
90
+ h2 {
91
+ margin: 0;
92
+ }
93
+
94
+ button {
95
+ margin-left: auto;
96
+ }
97
+ }
98
+
99
+ .cards {
100
+ display: grid;
101
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
102
+ gap: 1em;
103
+ }
104
+
105
+ @media (width < 700px) {
106
+ .sysadmin-main {
107
+ padding: 1em;
108
+ padding-bottom: 5em;
109
+ }
110
+
111
+ .cards {
112
+ grid-template-columns: 1fr;
113
+ }
114
+ }
115
+ </style>
@@ -0,0 +1,14 @@
1
+ import { fetchAPI } from '@axium/client/requests';
2
+
3
+ export const ssr = false;
4
+
5
+ export async function load({ parent }) {
6
+ const { session } = await parent();
7
+
8
+ const [systems, users] = await Promise.all([
9
+ fetchAPI('GET', 'users/:id/sysadmin/systems', {}, session.userId),
10
+ fetchAPI('GET', 'users/:id/sysadmin/users', {}, session.userId),
11
+ ]);
12
+
13
+ return { session, systems, users };
14
+ }
@@ -0,0 +1,92 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { Icon } from '@axium/client/components';
4
+ import { socket } from '@axium/client/socket';
5
+ import { SystemCard, SystemInitDialog } from '@axium/sysadmin/components';
6
+
7
+ const { data } = $props();
8
+
9
+ let systems = $state(data.systems);
10
+
11
+ const onlineHosts = $derived(await socket?.emitWithAck('sysadmin:ping').then(systems => systems.map(s => s.hostname)));
12
+ </script>
13
+
14
+ <svelte:head>
15
+ <title>{text('sysadmin.systems')}</title>
16
+ </svelte:head>
17
+
18
+ <div class="systems">
19
+ <p>
20
+ <a class="subtle icon-text back" href="/sysadmin">
21
+ <Icon i="arrow-left" />
22
+ <span>{text('sysadmin.back_to_main')}</span>
23
+ </a>
24
+ </p>
25
+
26
+ <div class="systems-header">
27
+ <h1>{text('sysadmin.systems')}</h1>
28
+ <button class="icon-text" command="show-modal" commandfor="system-init">
29
+ <Icon i="plus" />
30
+ <span>{text('sysadmin.new_system')}</span>
31
+ </button>
32
+ </div>
33
+
34
+ {#if systems.length}
35
+ <div class="cards">
36
+ {#each systems as system, i (system.id)}
37
+ <SystemCard
38
+ bind:system={systems[i]}
39
+ online={!!onlineHosts?.includes(system.hostname)}
40
+ deleted={() => systems.splice(i, 1)}
41
+ />
42
+ {/each}
43
+ </div>
44
+ {:else}
45
+ <p class="subtle">{text('sysadmin.no_systems')}</p>
46
+ {/if}
47
+ </div>
48
+
49
+ <SystemInitDialog userId={data.session.userId} created={system => systems.push(system)} />
50
+
51
+ <style>
52
+ .systems {
53
+ padding: 2em;
54
+ display: flex;
55
+ flex-direction: column;
56
+ gap: 1em;
57
+ }
58
+
59
+ .back {
60
+ width: fit-content;
61
+ }
62
+
63
+ .systems-header {
64
+ display: flex;
65
+ gap: 1em;
66
+
67
+ h1 {
68
+ margin: 0;
69
+ }
70
+
71
+ button {
72
+ margin-left: auto;
73
+ }
74
+ }
75
+
76
+ .cards {
77
+ display: grid;
78
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
79
+ gap: 1em;
80
+ }
81
+
82
+ @media (width < 700px) {
83
+ .systems {
84
+ padding: 1em;
85
+ padding-bottom: 5em;
86
+ }
87
+
88
+ .cards {
89
+ grid-template-columns: 1fr;
90
+ }
91
+ }
92
+ </style>
@@ -0,0 +1,11 @@
1
+ import { fetchAPI } from '@axium/client/requests';
2
+
3
+ export const ssr = false;
4
+
5
+ export async function load({ parent }) {
6
+ const { session } = await parent();
7
+
8
+ const systems = await fetchAPI('GET', 'users/:id/sysadmin/systems', {}, session.userId);
9
+
10
+ return { session, systems };
11
+ }
@@ -0,0 +1,290 @@
1
+ <script lang="ts">
2
+ import { text } from '@axium/client';
3
+ import { formatBytes, formatDuration } from '@axium/core';
4
+ import { Icon, NumberBar } from '@axium/client/components';
5
+ import { connect } from '@axium/client/socket';
6
+ import { systemTypeIcons, type SystemInfo } from '@axium/sysadmin';
7
+ import '@axium/sysadmin/common';
8
+
9
+ const { data } = $props();
10
+ const system = data.system;
11
+
12
+ let info = $state<SystemInfo>();
13
+ let error = $state(false);
14
+ let loading = $state(true);
15
+
16
+ /** The fraction used, for a NumberBar, clamped to [0, 1]. */
17
+ function fraction(used: bigint, total: bigint): number {
18
+ if (total <= 0n) return 0;
19
+ return Math.min(1, Number(used) / Number(total));
20
+ }
21
+
22
+ function usageText(used: bigint, total: bigint): string {
23
+ return text('sysadmin.system.usage', { used: formatBytes(used), total: formatBytes(total) });
24
+ }
25
+
26
+ const socket = await connect();
27
+
28
+ async function loadInfo() {
29
+ try {
30
+ const all = await socket.emitWithAck('sysadmin:getSystemInfo');
31
+ info = all.find(i => i.hostname === system.hostname);
32
+ if (!info) error = true;
33
+ } catch {
34
+ error = true;
35
+ } finally {
36
+ loading = false;
37
+ }
38
+ }
39
+
40
+ setInterval(loadInfo, 2500);
41
+
42
+ $effect(() => void loadInfo());
43
+ </script>
44
+
45
+ <svelte:head>
46
+ <title>{text('sysadmin.system.page_title', system)}</title>
47
+ </svelte:head>
48
+
49
+ <div class="system-page">
50
+ <a class="subtle icon-text back" href="/sysadmin/systems">
51
+ <Icon i="arrow-left" />
52
+ <span>{text('sysadmin.back_to_systems')}</span>
53
+ </a>
54
+
55
+ <div class="system-header">
56
+ <Icon i={systemTypeIcons[system.type]} --size="3em" />
57
+ <div>
58
+ <h1>{system.name}</h1>
59
+ <span class="subtle">{system.hostname}</span>
60
+ </div>
61
+ <span class={['status', info ? 'online' : 'offline']}>
62
+ <Icon i={info ? 'circle-check' : 'circle-xmark'} />
63
+ {info ? text('sysadmin.online') : text('sysadmin.offline')}
64
+ </span>
65
+ </div>
66
+
67
+ {#if loading}
68
+ <p class="subtle">{text('sysadmin.system.loading_info')}</p>
69
+ {:else if error}
70
+ <div class="notice">
71
+ <p class="subtle">{text('sysadmin.system.info_error')}</p>
72
+ <button class="icon-text" onclick={loadInfo}>
73
+ <Icon i="rotate-right" />
74
+ <span>{text('sysadmin.system.refresh')}</span>
75
+ </button>
76
+ </div>
77
+ {:else if info}
78
+ <section>
79
+ <h2><Icon i="microchip" /> {text('sysadmin.system.hardware')}</h2>
80
+
81
+ <div class="component">
82
+ <h3><Icon i="microchip" /> {text('sysadmin.system.cpu')}</h3>
83
+ {#each info.cpus as cpu}
84
+ <div class="line">
85
+ <span>{cpu.model}</span>
86
+ <span class="subtle">{text('sysadmin.system.cpu_cores', { count: cpu.cores })}</span>
87
+ </div>
88
+ {/each}
89
+ </div>
90
+
91
+ {#if info.gpus.length}
92
+ <div class="component">
93
+ <h3><Icon i="display" /> {text('sysadmin.system.gpu')}</h3>
94
+ {#each info.gpus as gpu}
95
+ <div class="line">
96
+ <span>{gpu.model}</span>
97
+ </div>
98
+ {#if gpu.vram}
99
+ <NumberBar
100
+ value={fraction(gpu.vram.used, gpu.vram.total)}
101
+ max={1}
102
+ text={usageText(gpu.vram.used, gpu.vram.total)}
103
+ />
104
+ {/if}
105
+ {/each}
106
+ </div>
107
+ {/if}
108
+
109
+ <div class="component">
110
+ <h3><Icon i="memory" /> {text('sysadmin.system.memory')}</h3>
111
+ <NumberBar
112
+ value={fraction(info.memory.used, info.memory.total)}
113
+ max={1}
114
+ text={usageText(info.memory.used, info.memory.total)}
115
+ />
116
+ {#if info.memory.swap}
117
+ <span class="subtle">{text('sysadmin.system.swap')}</span>
118
+ <NumberBar
119
+ value={fraction(info.memory.swap.used, info.memory.swap.total)}
120
+ max={1}
121
+ text={usageText(info.memory.swap.used, info.memory.swap.total)}
122
+ />
123
+ {/if}
124
+ </div>
125
+
126
+ {#if info.storage.length}
127
+ <div class="component">
128
+ <h3><Icon i="hard-drive" /> {text('sysadmin.system.storage')}</h3>
129
+ {#each info.storage as disk}
130
+ <div class="line">
131
+ <span>{disk.model}</span>
132
+ </div>
133
+ <NumberBar value={fraction(disk.used, disk.total)} max={1} text={usageText(disk.used, disk.total)} />
134
+ {/each}
135
+ </div>
136
+ {/if}
137
+
138
+ {#if info.networkInterfaces.length}
139
+ <div class="component">
140
+ <h3><Icon i="network-wired" /> {text('sysadmin.system.network')}</h3>
141
+ {#each info.networkInterfaces as iface}
142
+ <div class="line">
143
+ <span class="icon-text">
144
+ <Icon i={iface.wireless ? 'wifi' : 'ethernet'} />
145
+ {iface.name}
146
+ </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')}
150
+ </span>
151
+ {#if iface.speed}
152
+ <span>{iface.speed} Mbit/s</span>
153
+ {/if}
154
+ </div>
155
+ {/each}
156
+ </div>
157
+ {/if}
158
+ </section>
159
+
160
+ <section>
161
+ <h2><Icon i="layer-group" /> {text('sysadmin.system.os')}</h2>
162
+ <dl>
163
+ <dt>{text('sysadmin.system.platform')}</dt>
164
+ <dd>{info.type} <span class="subtle">({info.platform})</span></dd>
165
+ <dt>{text('sysadmin.system.release')}</dt>
166
+ <dd>{info.release} <span class="subtle">{info.version}</span></dd>
167
+ <dt>{text('sysadmin.system.arch')}</dt>
168
+ <dd>{info.arch} <span class="subtle">({info.machine})</span></dd>
169
+ <dt>{text('sysadmin.system.uptime')}</dt>
170
+ <dd>{formatDuration(info.uptime)}</dd>
171
+ </dl>
172
+ </section>
173
+ {/if}
174
+ </div>
175
+
176
+ <style>
177
+ .system-page {
178
+ padding: 2em;
179
+ display: flex;
180
+ flex-direction: column;
181
+ gap: 1.5em;
182
+ }
183
+
184
+ .back {
185
+ width: fit-content;
186
+ }
187
+
188
+ .system-header {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 1em;
192
+ }
193
+
194
+ .system-header :global(.Icon) {
195
+ flex-shrink: 0;
196
+ }
197
+
198
+ .system-header h1 {
199
+ margin: 0;
200
+ }
201
+
202
+ .status,
203
+ .net-status {
204
+ display: inline-flex;
205
+ align-items: center;
206
+ gap: 0.35em;
207
+ }
208
+
209
+ .status {
210
+ margin-left: auto;
211
+ }
212
+
213
+ .online {
214
+ --fill: var(--green);
215
+ color: var(--green);
216
+ }
217
+
218
+ .offline {
219
+ --fill: hsl(0 0 var(--fg-light));
220
+ color: hsl(0 0 var(--fg-light));
221
+ }
222
+
223
+ .notice {
224
+ display: flex;
225
+ flex-direction: column;
226
+ gap: 0.5em;
227
+ align-items: flex-start;
228
+ }
229
+
230
+ section {
231
+ display: flex;
232
+ flex-direction: column;
233
+ gap: 1em;
234
+
235
+ h2 {
236
+ margin: 0;
237
+ display: flex;
238
+ align-items: center;
239
+ gap: 0.5em;
240
+ }
241
+ }
242
+
243
+ .component {
244
+ display: flex;
245
+ flex-direction: column;
246
+ gap: 0.5em;
247
+ padding: 1em;
248
+ border-radius: 0.5em;
249
+ background-color: var(--bg-alt);
250
+
251
+ h3 {
252
+ margin: 0;
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 0.5em;
256
+ }
257
+ }
258
+
259
+ .line {
260
+ display: flex;
261
+ align-items: center;
262
+ gap: 1em;
263
+ flex-wrap: wrap;
264
+ .subtle {
265
+ margin-right: auto;
266
+ }
267
+ }
268
+
269
+ dl {
270
+ display: grid;
271
+ grid-template-columns: max-content 1fr;
272
+ gap: 0.5em 2em;
273
+ margin: 0;
274
+ }
275
+
276
+ dt {
277
+ font-weight: bold;
278
+ }
279
+
280
+ dd {
281
+ margin: 0;
282
+ }
283
+
284
+ @media (width < 700px) {
285
+ .system-page {
286
+ padding: 1em;
287
+ padding-bottom: 5em;
288
+ }
289
+ }
290
+ </style>
@@ -0,0 +1,11 @@
1
+ import { fetchAPI } from '@axium/client/requests';
2
+
3
+ export const ssr = false;
4
+
5
+ export async function load({ parent, params }) {
6
+ const { session } = await parent();
7
+
8
+ const system = await fetchAPI('GET', 'sysadmin/systems/:id', {}, params.id);
9
+
10
+ return { session, system };
11
+ }