@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
@@ -0,0 +1,136 @@
1
+ import * as acl from '@axium/server/acl';
2
+ import { authRequestForItem, checkAuthForUser } from '@axium/server/auth';
3
+ import { database } from '@axium/server/database';
4
+ import { parseBody, withError } from '@axium/server/requests';
5
+ import { addRoute } from '@axium/server/routes';
6
+ import * as z from 'zod';
7
+ import { SystemInit, SystemUserInit } from '../common.js';
8
+ function parseSystem(system, isShared) {
9
+ return Object.assign({ acl: [] }, system, { isShared });
10
+ }
11
+ addRoute({
12
+ path: '/api/users/:id/sysadmin/systems',
13
+ params: { id: z.uuid() },
14
+ async GET(request, { id: userId }) {
15
+ const { user } = await checkAuthForUser(request, userId);
16
+ const systems = await database
17
+ .selectFrom('systems')
18
+ .selectAll()
19
+ .select(acl.from('systems'))
20
+ .where(eb => eb.or([eb('userId', '=', userId), acl.existsIn('systems', user)(eb)]))
21
+ .execute()
22
+ .catch(withError('Could not get systems'));
23
+ return systems.map(system => parseSystem(system, system.userId !== userId));
24
+ },
25
+ async PUT(request, { id: userId }) {
26
+ const init = await parseBody(request, SystemInit);
27
+ await checkAuthForUser(request, userId);
28
+ return parseSystem(await database
29
+ .insertInto('systems')
30
+ .values({ ...init, userId })
31
+ .returningAll()
32
+ .executeTakeFirstOrThrow()
33
+ .catch(withError('Could not create system')), false);
34
+ },
35
+ });
36
+ addRoute({
37
+ path: '/api/sysadmin/systems/:id',
38
+ params: { id: z.uuid() },
39
+ async GET(request, { id }) {
40
+ const { item, fromACL } = await authRequestForItem(request, 'systems', id, { read: true });
41
+ return parseSystem(item, fromACL);
42
+ },
43
+ async PATCH(request, { id }) {
44
+ const init = await parseBody(request, SystemInit);
45
+ const { fromACL } = await authRequestForItem(request, 'systems', id, { edit: true });
46
+ const system = await database
47
+ .updateTable('systems')
48
+ .set(init)
49
+ .where('id', '=', id)
50
+ .returningAll()
51
+ .returning(acl.from('systems'))
52
+ .executeTakeFirstOrThrow()
53
+ .catch(withError('Could not update system'));
54
+ return parseSystem(system, fromACL);
55
+ },
56
+ async DELETE(request, { id }) {
57
+ const { fromACL } = await authRequestForItem(request, 'systems', id, { manage: true });
58
+ const system = await database
59
+ .deleteFrom('systems')
60
+ .where('id', '=', id)
61
+ .returningAll()
62
+ .returning(acl.from('systems'))
63
+ .executeTakeFirstOrThrow()
64
+ .catch(withError('Could not delete system'));
65
+ return parseSystem(system, fromACL);
66
+ },
67
+ });
68
+ addRoute({
69
+ path: '/api/users/:id/sysadmin/users',
70
+ params: { id: z.uuid() },
71
+ async GET(request, { id: userId }) {
72
+ await checkAuthForUser(request, userId);
73
+ return await database
74
+ .selectFrom('system_users')
75
+ .selectAll()
76
+ .where('userId', '=', userId)
77
+ .execute()
78
+ .catch(withError('Could not get system users'));
79
+ },
80
+ async PUT(request, { id: userId }) {
81
+ const init = await parseBody(request, SystemUserInit);
82
+ await checkAuthForUser(request, userId);
83
+ return await database
84
+ .insertInto('system_users')
85
+ .values({ ...init, userId })
86
+ .returningAll()
87
+ .executeTakeFirstOrThrow()
88
+ .catch(withError('Could not create system user'));
89
+ },
90
+ });
91
+ addRoute({
92
+ path: '/api/sysadmin/users/:id',
93
+ params: { id: z.uuid() },
94
+ async GET(request, { id }) {
95
+ const user = await database
96
+ .selectFrom('system_users')
97
+ .selectAll()
98
+ .where('id', '=', id)
99
+ .executeTakeFirstOrThrow()
100
+ .catch(withError('Could not get system user', 404));
101
+ await checkAuthForUser(request, user.userId);
102
+ return user;
103
+ },
104
+ async PATCH(request, { id }) {
105
+ const init = await parseBody(request, SystemUserInit);
106
+ const existing = await database
107
+ .selectFrom('system_users')
108
+ .select('userId')
109
+ .where('id', '=', id)
110
+ .executeTakeFirstOrThrow()
111
+ .catch(withError('Could not get system user', 404));
112
+ await checkAuthForUser(request, existing.userId);
113
+ return await database
114
+ .updateTable('system_users')
115
+ .set(init)
116
+ .where('id', '=', id)
117
+ .returningAll()
118
+ .executeTakeFirstOrThrow()
119
+ .catch(withError('Could not update system user'));
120
+ },
121
+ async DELETE(request, { id }) {
122
+ const existing = await database
123
+ .selectFrom('system_users')
124
+ .select('userId')
125
+ .where('id', '=', id)
126
+ .executeTakeFirstOrThrow()
127
+ .catch(withError('Could not get system user', 404));
128
+ await checkAuthForUser(request, existing.userId);
129
+ return await database
130
+ .deleteFrom('system_users')
131
+ .where('id', '=', id)
132
+ .returningAll()
133
+ .executeTakeFirstOrThrow()
134
+ .catch(withError('Could not delete system user'));
135
+ },
136
+ });
@@ -0,0 +1,7 @@
1
+ import type { FromFile as FromSchemaFile } from '@axium/server/db/schema';
2
+ import type schema from '../../db.json';
3
+ import './socket.js';
4
+ declare module '@axium/server/database' {
5
+ interface Schema extends FromSchemaFile<typeof schema> {
6
+ }
7
+ }
@@ -0,0 +1 @@
1
+ import './socket.js';
@@ -0,0 +1,3 @@
1
+ import './socket.js';
2
+ import './db.js';
3
+ import './api.js';
@@ -0,0 +1,3 @@
1
+ import './socket.js';
2
+ import './db.js';
3
+ import './api.js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { addListener, io } from '@axium/server/socket';
2
+ addListener('sysadmin:ping', async (socket, cb) => {
3
+ const result = await io.in(`local:${socket.data.user.id}`).timeout(5000).emitWithAck('sysadmin:ping');
4
+ cb(result);
5
+ });
6
+ addListener('sysadmin:getSystemInfo', async (socket, cb) => {
7
+ const result = await io.in(`local:${socket.data.user.id}`).timeout(5000).emitWithAck('sysadmin:getSystemInfo');
8
+ cb(result);
9
+ });
@@ -0,0 +1,8 @@
1
+ import en from '../locales/en.json';
2
+ import './common.js';
3
+ type en = typeof en;
4
+ declare module '@axium/client/locales' {
5
+ interface Locale extends en {
6
+ }
7
+ }
8
+ export {};
@@ -0,0 +1,4 @@
1
+ import { extendLocale } from '@axium/client';
2
+ import en from '../locales/en.json' with { type: 'json' };
3
+ import './common.js';
4
+ extendLocale('en', en);
@@ -0,0 +1,133 @@
1
+ <script lang="ts">
2
+ import { fetchAPI, text } from '@axium/client';
3
+ import { contextMenu, type ContextMenuItem } from '@axium/client/attachments';
4
+ import { Icon, Popover } from '@axium/client/components';
5
+ import { copy } from '@axium/client/gui';
6
+ import { toastStatus } from '@axium/client/toast';
7
+ import { systemTypeIcons, type System } from '@axium/sysadmin';
8
+ import SystemInitDialog from './SystemInitDialog.svelte';
9
+
10
+ let {
11
+ system = $bindable(),
12
+ online,
13
+ edited,
14
+ deleted,
15
+ }: {
16
+ system: System;
17
+ online: boolean;
18
+ /** Called with the updated system after an edit */
19
+ edited?(system: System): unknown;
20
+ /** Called after the system is deleted */
21
+ deleted?(system: System): unknown;
22
+ } = $props();
23
+
24
+ let editDialog = $state<HTMLDialogElement>();
25
+
26
+ const link = $derived(`/sysadmin/systems/${system.id}`);
27
+
28
+ function remove() {
29
+ toastStatus(
30
+ fetchAPI('DELETE', 'sysadmin/systems/:id', {}, system.id).then(() => deleted?.(system)),
31
+ text('sysadmin.SystemCard.toast_deleted')
32
+ );
33
+ }
34
+
35
+ /** The actions shown in both the hover popover and the right-click context menu. */
36
+ const menuItems = (): ContextMenuItem[] => [
37
+ { i: 'pen', text: text('sysadmin.SystemCard.edit'), action: () => editDialog!.showModal() },
38
+ { i: 'link-horizontal', text: text('sysadmin.SystemCard.copy_link'), action: () => copy('text/plain', link) },
39
+ {
40
+ i: 'arrow-up-right-from-square',
41
+ text: text('sysadmin.SystemCard.open_new_tab'),
42
+ action: () => window.open(`/sysadmin/systems/${system.id}`),
43
+ },
44
+ { i: 'trash', text: text('generic.delete'), danger: true, action: remove },
45
+ ];
46
+ </script>
47
+
48
+ <div class="SystemCard" {@attach contextMenu(menuItems)}>
49
+ <span class={['indicator', online ? 'online' : 'offline']} title={online ? text('sysadmin.online') : text('sysadmin.offline')}></span>
50
+ <a class="link" href="/sysadmin/systems/{system.id}">
51
+ <Icon i={systemTypeIcons[system.type]} class="icon" --size="2em" />
52
+ <div class="info">
53
+ <span class="name">{system.name}</span>
54
+ <span class="subtle hostname">{system.hostname}</span>
55
+ </div>
56
+ </a>
57
+
58
+ <Popover showToggle="hover">
59
+ {#each menuItems() as item}
60
+ <div class={['menu-item', item.danger && 'danger']} onclick={item.action}>
61
+ <Icon i={item.i!} />
62
+ <span>{item.text}</span>
63
+ </div>
64
+ {/each}
65
+ </Popover>
66
+ </div>
67
+
68
+ <SystemInitDialog
69
+ bind:dialog={editDialog}
70
+ {system}
71
+ edited={updated => {
72
+ system = updated;
73
+ edited?.(updated);
74
+ }}
75
+ />
76
+
77
+ <style>
78
+ .SystemCard {
79
+ position: relative;
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 0.5em;
83
+ padding: 1em;
84
+ border-radius: 0.5em;
85
+ background-color: var(--bg-alt);
86
+ }
87
+
88
+ .link {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 1em;
92
+ flex: 1;
93
+ min-width: 0;
94
+ cursor: pointer;
95
+ text-decoration: none;
96
+ color: inherit;
97
+ }
98
+
99
+ .indicator {
100
+ position: absolute;
101
+ top: 0.6em;
102
+ left: 0.6em;
103
+ width: 0.6em;
104
+ height: 0.6em;
105
+ border-radius: 50%;
106
+
107
+ &.online {
108
+ background-color: var(--green);
109
+ }
110
+
111
+ &.offline {
112
+ background-color: hsl(0 0 calc(var(--fg-light) - 30));
113
+ }
114
+ }
115
+
116
+ .info {
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 0.1em;
120
+ flex: 1;
121
+ min-width: 0;
122
+ }
123
+
124
+ .name {
125
+ font-weight: bold;
126
+ }
127
+
128
+ .hostname {
129
+ overflow: hidden;
130
+ text-overflow: ellipsis;
131
+ white-space: nowrap;
132
+ }
133
+ </style>
@@ -0,0 +1,59 @@
1
+ <script lang="ts">
2
+ import { fetchAPI, text } from '@axium/client';
3
+ import { FormDialog } from '@axium/client/components';
4
+ import { SystemInit, SystemType, type System } from '@axium/sysadmin';
5
+
6
+ let {
7
+ dialog = $bindable(),
8
+ userId,
9
+ system,
10
+ created,
11
+ edited,
12
+ }: {
13
+ /** The dialog element, for triggering programmatically (e.g. an edit action) */
14
+ dialog?: HTMLDialogElement;
15
+ /** The user the system is created for (used in create mode) */
16
+ userId?: string;
17
+ /** An existing system to edit. When set, the dialog operates in edit mode. */
18
+ system?: System;
19
+ /** Called with the newly created system (create mode) */
20
+ created?(system: System): unknown;
21
+ /** Called with the updated system (edit mode) */
22
+ edited?(system: System): unknown;
23
+ } = $props();
24
+
25
+ const types = SystemType.def.values;
26
+ </script>
27
+
28
+ <FormDialog
29
+ id={system ? undefined : 'system-init'}
30
+ bind:dialog
31
+ submitText={system ? text('sysadmin.SystemInitDialog.save') : text('sysadmin.SystemInitDialog.submit')}
32
+ submit={async raw => {
33
+ const init = SystemInit.parse(raw);
34
+ if (system) {
35
+ const updated = await fetchAPI('PATCH', 'sysadmin/systems/:id', init, system.id);
36
+ await edited?.(updated);
37
+ } else {
38
+ const created_ = await fetchAPI('PUT', 'users/:id/sysadmin/systems', init, userId!);
39
+ await created?.(created_);
40
+ }
41
+ }}
42
+ >
43
+ <div>
44
+ <label for="name">{text('sysadmin.SystemInitDialog.name')}</label>
45
+ <input name="name" type="text" required value={system?.name ?? ''} />
46
+ </div>
47
+ <div>
48
+ <label for="hostname">{text('sysadmin.SystemInitDialog.hostname')}</label>
49
+ <input name="hostname" type="text" required value={system?.hostname ?? ''} />
50
+ </div>
51
+ <div>
52
+ <label for="type">{text('sysadmin.SystemInitDialog.type')}</label>
53
+ <select name="type" value={system?.type ?? 'server'}>
54
+ {#each types as type}
55
+ <option value={type}>{text(`sysadmin.system_type.${type}`)}</option>
56
+ {/each}
57
+ </select>
58
+ </div>
59
+ </FormDialog>
@@ -0,0 +1,108 @@
1
+ <script lang="ts">
2
+ import { fetchAPI, text } from '@axium/client';
3
+ import { contextMenu, type ContextMenuItem } from '@axium/client/attachments';
4
+ import { Icon, Popover } from '@axium/client/components';
5
+ import { copy } from '@axium/client/gui';
6
+ import { toastStatus } from '@axium/client/toast';
7
+ import type { SystemUser } from '@axium/sysadmin';
8
+ import UserInitDialog from './UserInitDialog.svelte';
9
+
10
+ let {
11
+ user = $bindable(),
12
+ edited,
13
+ deleted,
14
+ }: {
15
+ user: SystemUser;
16
+ /** Called with the updated user after an edit */
17
+ edited?(user: SystemUser): unknown;
18
+ /** Called after the user is deleted */
19
+ deleted?(user: SystemUser): unknown;
20
+ } = $props();
21
+
22
+ let editDialog = $state<HTMLDialogElement>();
23
+
24
+ const link = $derived(`/sysadmin/users/${user.id}`);
25
+
26
+ function remove() {
27
+ toastStatus(
28
+ fetchAPI('DELETE', 'sysadmin/users/:id', {}, user.id).then(() => deleted?.(user)),
29
+ text('sysadmin.UserCard.toast_deleted')
30
+ );
31
+ }
32
+
33
+ /** The actions shown in both the hover popover and the right-click context menu. */
34
+ const menuItems = (): ContextMenuItem[] => [
35
+ { i: 'pen', text: text('sysadmin.UserCard.edit'), action: () => editDialog!.showModal() },
36
+ { i: 'link-horizontal', text: text('sysadmin.UserCard.copy_link'), action: () => copy('text/plain', link) },
37
+ { i: 'arrow-up-right-from-square', text: text('sysadmin.UserCard.open_new_tab'), action: () => window.open(link) },
38
+ { i: 'trash', text: text('generic.delete'), danger: true, action: remove },
39
+ ];
40
+ </script>
41
+
42
+ <div class="UserCard" {@attach contextMenu(menuItems)}>
43
+ <a class="link" href={link}>
44
+ <Icon i="user" class="icon" --size="2em" />
45
+ <div class="info">
46
+ <span class="name">{user.name}</span>
47
+ <span class="subtle username">{user.username}</span>
48
+ </div>
49
+ </a>
50
+
51
+ <Popover showToggle="hover">
52
+ {#each menuItems() as item}
53
+ <div class={['menu-item', item.danger && 'danger']} onclick={item.action}>
54
+ <Icon i={item.i!} />
55
+ <span>{item.text}</span>
56
+ </div>
57
+ {/each}
58
+ </Popover>
59
+ </div>
60
+
61
+ <UserInitDialog
62
+ bind:dialog={editDialog}
63
+ {user}
64
+ edited={updated => {
65
+ user = updated;
66
+ edited?.(updated);
67
+ }}
68
+ />
69
+
70
+ <style>
71
+ .UserCard {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 0.5em;
75
+ padding: 1em;
76
+ border-radius: 0.5em;
77
+ background-color: var(--bg-alt);
78
+ }
79
+
80
+ .link {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 1em;
84
+ flex: 1;
85
+ min-width: 0;
86
+ cursor: pointer;
87
+ text-decoration: none;
88
+ color: inherit;
89
+ }
90
+
91
+ .info {
92
+ display: flex;
93
+ flex-direction: column;
94
+ gap: 0.1em;
95
+ flex: 1;
96
+ min-width: 0;
97
+ }
98
+
99
+ .name {
100
+ font-weight: bold;
101
+ }
102
+
103
+ .username {
104
+ overflow: hidden;
105
+ text-overflow: ellipsis;
106
+ white-space: nowrap;
107
+ }
108
+ </style>
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import { fetchAPI, text } from '@axium/client';
3
+ import { FormDialog } from '@axium/client/components';
4
+ import { SystemUserInit, type SystemUser } from '@axium/sysadmin';
5
+
6
+ let {
7
+ dialog = $bindable(),
8
+ userId,
9
+ user,
10
+ created,
11
+ edited,
12
+ }: {
13
+ /** The dialog element, for triggering programmatically (e.g. an edit action) */
14
+ dialog?: HTMLDialogElement;
15
+ /** The user the system user is created for (used in create mode) */
16
+ userId?: string;
17
+ /** An existing system user to edit. When set, the dialog operates in edit mode. */
18
+ user?: SystemUser;
19
+ /** Called with the newly created system user (create mode) */
20
+ created?(user: SystemUser): unknown;
21
+ /** Called with the updated system user (edit mode) */
22
+ edited?(user: SystemUser): unknown;
23
+ } = $props();
24
+ </script>
25
+
26
+ <FormDialog
27
+ id={user ? undefined : 'system-user-init'}
28
+ bind:dialog
29
+ submitText={user ? text('sysadmin.UserInitDialog.save') : text('sysadmin.UserInitDialog.submit')}
30
+ submit={async raw => {
31
+ const init = SystemUserInit.parse(raw);
32
+ if (user) {
33
+ const updated = await fetchAPI('PATCH', 'sysadmin/users/:id', init, user.id);
34
+ await edited?.(updated);
35
+ } else {
36
+ const created_ = await fetchAPI('PUT', 'users/:id/sysadmin/users', init, userId!);
37
+ await created?.(created_);
38
+ }
39
+ }}
40
+ >
41
+ <div>
42
+ <label for="name">{text('sysadmin.UserInitDialog.name')}</label>
43
+ <input name="name" type="text" required value={user?.name ?? ''} />
44
+ </div>
45
+ <div>
46
+ <label for="username">{text('sysadmin.UserInitDialog.username')}</label>
47
+ <input name="username" type="text" required value={user?.username ?? ''} />
48
+ </div>
49
+ </FormDialog>
package/lib/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { default as SystemCard } from './SystemCard.svelte';
2
+ export { default as SystemInitDialog } from './SystemInitDialog.svelte';
3
+ export { default as UserCard } from './UserCard.svelte';
4
+ export { default as UserInitDialog } from './UserInitDialog.svelte';
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": ["../tsconfig.json", "../.svelte-kit/tsconfig.json"],
3
+ "compilerOptions": {
4
+ "rootDir": "..",
5
+ "noEmit": true,
6
+ "module": "preserve",
7
+ "moduleResolution": "Bundler"
8
+ },
9
+ "include": ["**/*.svelte", "**/*.ts"],
10
+ "exclude": [],
11
+ "references": [{ "path": ".." }]
12
+ }
@@ -0,0 +1,79 @@
1
+ {
2
+ "app_name": {
3
+ "sysadmin": "System Administration"
4
+ },
5
+ "sysadmin": {
6
+ "page_title": "System Administration",
7
+ "systems": "Systems",
8
+ "system_users": "System Users",
9
+ "view_all": "View all",
10
+ "no_systems": "No systems yet.",
11
+ "no_system_users": "No system users yet.",
12
+ "new_system": "Add System",
13
+ "new_system_user": "Add User",
14
+ "online": "Online",
15
+ "offline": "Offline",
16
+ "back_to_main": "Back to System Administration",
17
+ "back_to_systems": "Back to Systems",
18
+ "back_to_users": "Back to System Users",
19
+ "SystemInitDialog": {
20
+ "submit": "Add System",
21
+ "save": "Save",
22
+ "name": "Name",
23
+ "hostname": "Hostname",
24
+ "type": "Type"
25
+ },
26
+ "SystemCard": {
27
+ "edit": "Edit",
28
+ "copy_link": "Copy Link",
29
+ "open_new_tab": "Open in New Tab",
30
+ "toast_deleted": "System deleted"
31
+ },
32
+ "UserInitDialog": {
33
+ "submit": "Add User",
34
+ "save": "Save",
35
+ "name": "Name",
36
+ "username": "Username"
37
+ },
38
+ "UserCard": {
39
+ "edit": "Edit",
40
+ "copy_link": "Copy Link",
41
+ "open_new_tab": "Open in New Tab",
42
+ "toast_deleted": "User deleted"
43
+ },
44
+ "user": {
45
+ "page_title": "{name} — System Users"
46
+ },
47
+ "system_type": {
48
+ "server": "Server",
49
+ "pc": "PC",
50
+ "laptop": "Laptop"
51
+ },
52
+ "system": {
53
+ "page_title": "{name} — Systems",
54
+ "offline_notice": "This system is offline. Live information is unavailable.",
55
+ "loading_info": "Loading system information…",
56
+ "info_error": "Could not load system information.",
57
+ "refresh": "Refresh",
58
+ "hardware": "Hardware",
59
+ "cpu": "CPU",
60
+ "cpu_cores": "{count} cores",
61
+ "gpu": "Graphics",
62
+ "memory": "Memory",
63
+ "swap": "Swap",
64
+ "storage": "Storage",
65
+ "network": "Network",
66
+ "wireless": "Wireless",
67
+ "wired": "Wired",
68
+ "connected": "Connected",
69
+ "disconnected": "Disconnected",
70
+ "os": "Operating System",
71
+ "platform": "Platform",
72
+ "release": "Release",
73
+ "kernel": "Kernel",
74
+ "arch": "Architecture",
75
+ "uptime": "Uptime",
76
+ "usage": "{used} / {total}"
77
+ }
78
+ }
79
+ }