sh3-core 0.5.2 → 0.5.5
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/Shell.svelte +6 -3
- package/dist/admin/AuthSettingsView.svelte +105 -0
- package/dist/admin/AuthSettingsView.svelte.d.ts +3 -0
- package/dist/admin/SystemView.svelte +73 -0
- package/dist/admin/SystemView.svelte.d.ts +3 -0
- package/dist/admin/UsersView.svelte +189 -0
- package/dist/admin/UsersView.svelte.d.ts +3 -0
- package/dist/admin/adminApp.d.ts +7 -0
- package/dist/admin/adminApp.js +24 -0
- package/dist/admin/adminShard.svelte.d.ts +4 -0
- package/dist/admin/adminShard.svelte.js +52 -0
- package/dist/api.d.ts +2 -1
- package/dist/api.js +1 -1
- package/dist/apps/lifecycle.d.ts +6 -1
- package/dist/apps/lifecycle.js +28 -4
- package/dist/apps/registry.svelte.d.ts +5 -2
- package/dist/apps/registry.svelte.js +6 -7
- package/dist/apps/types.d.ts +13 -0
- package/dist/auth/GuestBanner.svelte +144 -0
- package/dist/auth/GuestBanner.svelte.d.ts +3 -0
- package/dist/auth/SignInWall.svelte +213 -0
- package/dist/auth/SignInWall.svelte.d.ts +8 -0
- package/dist/auth/auth.svelte.d.ts +42 -31
- package/dist/auth/auth.svelte.js +106 -89
- package/dist/auth/index.d.ts +2 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/types.d.ts +41 -0
- package/dist/auth/types.js +6 -0
- package/dist/build.js +70 -16
- package/dist/createShell.d.ts +2 -2
- package/dist/createShell.js +78 -33
- package/dist/diagnostic/DiagnosticPromptModal.svelte +1 -1
- package/dist/host-entry.d.ts +2 -1
- package/dist/host-entry.js +2 -2
- package/dist/host.d.ts +0 -2
- package/dist/host.js +11 -25
- package/dist/layout/DragPreview.svelte +1 -1
- package/dist/overlays/ModalFrame.svelte +1 -1
- package/dist/overlays/PopupFrame.svelte +1 -1
- package/dist/overlays/ToastItem.svelte +1 -1
- package/dist/primitives/TabbedPanel.svelte +1 -1
- package/dist/registry/installer.js +0 -2
- package/dist/shards/activate.svelte.d.ts +13 -6
- package/dist/shards/activate.svelte.js +19 -8
- package/dist/shards/types.d.ts +11 -0
- package/dist/shell-shard/ShellHome.svelte +32 -118
- package/dist/store/InstalledView.svelte +7 -7
- package/dist/store/StoreView.svelte +16 -16
- package/dist/store/storeApp.js +1 -1
- package/dist/store/storeShard.svelte.js +5 -4
- package/dist/tokens.css +14 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/Shell.svelte
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { registerLayerRoot, unregisterLayerRoot } from './overlays/roots';
|
|
20
20
|
import { returnToHome, getActiveApp } from './api';
|
|
21
21
|
import iconsUrl from './assets/icons.svg';
|
|
22
|
+
import GuestBanner from './auth/GuestBanner.svelte';
|
|
22
23
|
|
|
23
24
|
// Layer metadata — order matches the stack in docs/design/layout.md.
|
|
24
25
|
// Index 0 here is layer 1 (floating panels); layer 0 is the content area.
|
|
@@ -64,6 +65,8 @@
|
|
|
64
65
|
{/if}
|
|
65
66
|
</header>
|
|
66
67
|
|
|
68
|
+
<GuestBanner />
|
|
69
|
+
|
|
67
70
|
<main class="shell-content" data-shell-region="content" data-shell-layer="0">
|
|
68
71
|
<LayoutRenderer />
|
|
69
72
|
</main>
|
|
@@ -97,7 +100,7 @@
|
|
|
97
100
|
<style>
|
|
98
101
|
.shell {
|
|
99
102
|
display: grid;
|
|
100
|
-
grid-template-rows: var(--shell-tabbar-height) 1fr var(--shell-statusbar-height);
|
|
103
|
+
grid-template-rows: var(--shell-tabbar-height) auto 1fr var(--shell-statusbar-height);
|
|
101
104
|
height: 100%;
|
|
102
105
|
width: 100%;
|
|
103
106
|
position: relative;
|
|
@@ -160,7 +163,7 @@
|
|
|
160
163
|
background: transparent;
|
|
161
164
|
color: var(--shell-fg-muted);
|
|
162
165
|
border: 1px solid var(--shell-border);
|
|
163
|
-
border-radius:
|
|
166
|
+
border-radius: var(--shell-radius);
|
|
164
167
|
cursor: pointer;
|
|
165
168
|
}
|
|
166
169
|
.shell-tabbar-home-button:hover {
|
|
@@ -180,6 +183,6 @@
|
|
|
180
183
|
color: #fff;
|
|
181
184
|
background: var(--shell-accent);
|
|
182
185
|
padding: 1px 6px;
|
|
183
|
-
border-radius:
|
|
186
|
+
border-radius: var(--shell-radius-lg);
|
|
184
187
|
}
|
|
185
188
|
</style>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Admin Auth Settings view — toggle auth-related global settings.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { GlobalSettings } from '../auth/types';
|
|
7
|
+
|
|
8
|
+
let settings = $state<GlobalSettings | null>(null);
|
|
9
|
+
let loading = $state(true);
|
|
10
|
+
let saving = $state(false);
|
|
11
|
+
let error = $state<string | null>(null);
|
|
12
|
+
|
|
13
|
+
async function fetchSettings() {
|
|
14
|
+
loading = true;
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch('/api/admin/settings', { credentials: 'include' });
|
|
17
|
+
if (!res.ok) throw new Error('Failed to fetch settings');
|
|
18
|
+
settings = await res.json();
|
|
19
|
+
} catch (err) {
|
|
20
|
+
error = err instanceof Error ? err.message : 'Failed to load settings';
|
|
21
|
+
} finally {
|
|
22
|
+
loading = false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function save() {
|
|
27
|
+
if (!settings) return;
|
|
28
|
+
saving = true;
|
|
29
|
+
error = null;
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch('/api/admin/settings', {
|
|
32
|
+
method: 'PUT',
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
credentials: 'include',
|
|
35
|
+
body: JSON.stringify(settings),
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) throw new Error('Failed to save settings');
|
|
38
|
+
settings = await res.json();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
error = err instanceof Error ? err.message : 'Failed to save';
|
|
41
|
+
} finally {
|
|
42
|
+
saving = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fetchSettings();
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div class="admin-auth">
|
|
50
|
+
<h2>Auth Settings</h2>
|
|
51
|
+
|
|
52
|
+
{#if loading}
|
|
53
|
+
<p class="admin-muted">Loading...</p>
|
|
54
|
+
{:else if settings}
|
|
55
|
+
<div class="admin-auth-fields">
|
|
56
|
+
<label class="admin-toggle">
|
|
57
|
+
<input type="checkbox" bind:checked={settings.auth.required} />
|
|
58
|
+
<span>Require sign-in</span>
|
|
59
|
+
<span class="admin-hint">When enabled, unauthenticated visitors see a sign-in wall.</span>
|
|
60
|
+
</label>
|
|
61
|
+
|
|
62
|
+
<label class="admin-toggle">
|
|
63
|
+
<input type="checkbox" bind:checked={settings.auth.guestAllowed} />
|
|
64
|
+
<span>Allow guest browsing</span>
|
|
65
|
+
<span class="admin-hint">When sign-in is required, guests can still browse with session-only data.</span>
|
|
66
|
+
</label>
|
|
67
|
+
|
|
68
|
+
<label class="admin-toggle">
|
|
69
|
+
<input type="checkbox" bind:checked={settings.auth.selfRegistration} />
|
|
70
|
+
<span>Self-registration</span>
|
|
71
|
+
<span class="admin-hint">Visitors can create their own accounts from the sign-in screen.</span>
|
|
72
|
+
</label>
|
|
73
|
+
|
|
74
|
+
<label class="admin-field">
|
|
75
|
+
<span>Session lifetime (hours)</span>
|
|
76
|
+
<input type="number" class="admin-input admin-input-sm" min="1" max="8760" bind:value={settings.auth.sessionTTL} />
|
|
77
|
+
</label>
|
|
78
|
+
|
|
79
|
+
<button type="button" class="admin-btn" onclick={save} disabled={saving}>
|
|
80
|
+
{saving ? 'Saving...' : 'Save'}
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{#if error}
|
|
85
|
+
<div class="admin-error">{error}</div>
|
|
86
|
+
{/if}
|
|
87
|
+
{/if}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<style>
|
|
91
|
+
.admin-auth { padding: 24px; font-family: system-ui, sans-serif; color: var(--shell-fg); }
|
|
92
|
+
.admin-auth h2 { margin: 0 0 16px; font-size: 18px; }
|
|
93
|
+
.admin-auth-fields { display: flex; flex-direction: column; gap: 16px; max-width: 480px; }
|
|
94
|
+
.admin-toggle { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; cursor: pointer; }
|
|
95
|
+
.admin-toggle input { accent-color: var(--shell-accent, #7c7cf0); }
|
|
96
|
+
.admin-hint { flex-basis: 100%; font-size: 11px; color: var(--shell-fg-muted); margin-left: 24px; }
|
|
97
|
+
.admin-field { display: flex; flex-direction: column; gap: 4px; }
|
|
98
|
+
.admin-field span { font-size: 13px; }
|
|
99
|
+
.admin-input { padding: 8px 12px; background: var(--shell-bg); color: var(--shell-fg); border: 1px solid var(--shell-border); border-radius: var(--shell-radius, 6px); font-size: 13px; }
|
|
100
|
+
.admin-input-sm { max-width: 120px; }
|
|
101
|
+
.admin-btn { padding: 8px 16px; background: var(--shell-accent, #7c7cf0); color: var(--shell-bg); border: none; border-radius: var(--shell-radius, 6px); font-weight: 600; cursor: pointer; align-self: flex-start; }
|
|
102
|
+
.admin-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
103
|
+
.admin-error { margin-top: 8px; color: var(--shell-error, #d32f2f); font-size: 13px; }
|
|
104
|
+
.admin-muted { color: var(--shell-fg-muted); font-style: italic; }
|
|
105
|
+
</style>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Admin System view — server status and restart.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let version = $state('...');
|
|
7
|
+
let restarting = $state(false);
|
|
8
|
+
let restartError = $state<string | null>(null);
|
|
9
|
+
|
|
10
|
+
async function fetchVersion() {
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch('/api/version');
|
|
13
|
+
if (res.ok) {
|
|
14
|
+
const body = await res.json();
|
|
15
|
+
version = body.version;
|
|
16
|
+
}
|
|
17
|
+
} catch { /* ignore */ }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function restart() {
|
|
21
|
+
restarting = true;
|
|
22
|
+
restartError = null;
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch('/api/admin/restart', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
credentials: 'include',
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const body = await res.json().catch(() => ({}));
|
|
30
|
+
restartError = body.error || 'Restart failed';
|
|
31
|
+
restarting = false;
|
|
32
|
+
}
|
|
33
|
+
// If 202, server will restart — page will reconnect
|
|
34
|
+
} catch {
|
|
35
|
+
restartError = 'Network error';
|
|
36
|
+
restarting = false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fetchVersion();
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<div class="admin-system">
|
|
44
|
+
<h2>System</h2>
|
|
45
|
+
|
|
46
|
+
<div class="admin-system-info">
|
|
47
|
+
<div class="admin-system-row">
|
|
48
|
+
<span class="admin-system-label">Server version</span>
|
|
49
|
+
<span>{version}</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="admin-system-actions">
|
|
54
|
+
<button type="button" class="admin-btn-danger" onclick={restart} disabled={restarting}>
|
|
55
|
+
{restarting ? 'Restarting...' : 'Restart server'}
|
|
56
|
+
</button>
|
|
57
|
+
{#if restartError}
|
|
58
|
+
<div class="admin-error">{restartError}</div>
|
|
59
|
+
{/if}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<style>
|
|
64
|
+
.admin-system { padding: 24px; font-family: system-ui, sans-serif; color: var(--shell-fg); }
|
|
65
|
+
.admin-system h2 { margin: 0 0 16px; font-size: 18px; }
|
|
66
|
+
.admin-system-info { margin-bottom: 24px; }
|
|
67
|
+
.admin-system-row { display: flex; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--shell-border, #3a3a5c); font-size: 13px; }
|
|
68
|
+
.admin-system-label { color: var(--shell-fg-subtle); min-width: 140px; }
|
|
69
|
+
.admin-system-actions { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }
|
|
70
|
+
.admin-btn-danger { padding: 8px 16px; background: transparent; color: var(--shell-error, #d32f2f); border: 1px solid var(--shell-error, #d32f2f); border-radius: var(--shell-radius, 6px); font-weight: 600; cursor: pointer; }
|
|
71
|
+
.admin-btn-danger:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
72
|
+
.admin-error { color: var(--shell-error, #d32f2f); font-size: 13px; }
|
|
73
|
+
</style>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Admin Users view — list, create, edit, delete users.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getAuthHeader } from '../auth/index';
|
|
7
|
+
import type { AuthUser } from '../auth/types';
|
|
8
|
+
|
|
9
|
+
let users = $state<AuthUser[]>([]);
|
|
10
|
+
let loading = $state(true);
|
|
11
|
+
let error = $state<string | null>(null);
|
|
12
|
+
|
|
13
|
+
// Create form
|
|
14
|
+
let showCreate = $state(false);
|
|
15
|
+
let newUsername = $state('');
|
|
16
|
+
let newDisplayName = $state('');
|
|
17
|
+
let newPassword = $state('');
|
|
18
|
+
let newRole = $state<'admin' | 'user'>('user');
|
|
19
|
+
let createError = $state<string | null>(null);
|
|
20
|
+
|
|
21
|
+
// Edit state
|
|
22
|
+
let editingId = $state<string | null>(null);
|
|
23
|
+
let editDisplayName = $state('');
|
|
24
|
+
let editRole = $state<'admin' | 'user'>('user');
|
|
25
|
+
let editPassword = $state('');
|
|
26
|
+
|
|
27
|
+
async function fetchUsers() {
|
|
28
|
+
loading = true;
|
|
29
|
+
error = null;
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch('/api/admin/users', { credentials: 'include' });
|
|
32
|
+
if (!res.ok) throw new Error('Failed to fetch users');
|
|
33
|
+
users = await res.json();
|
|
34
|
+
} catch (err) {
|
|
35
|
+
error = err instanceof Error ? err.message : 'Failed to load users';
|
|
36
|
+
} finally {
|
|
37
|
+
loading = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function createUser() {
|
|
42
|
+
createError = null;
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch('/api/admin/users', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
credentials: 'include',
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
username: newUsername,
|
|
50
|
+
displayName: newDisplayName || newUsername,
|
|
51
|
+
password: newPassword,
|
|
52
|
+
role: newRole,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const body = await res.json().catch(() => ({}));
|
|
57
|
+
createError = body.error || 'Failed to create user';
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
newUsername = '';
|
|
61
|
+
newDisplayName = '';
|
|
62
|
+
newPassword = '';
|
|
63
|
+
newRole = 'user';
|
|
64
|
+
showCreate = false;
|
|
65
|
+
await fetchUsers();
|
|
66
|
+
} catch {
|
|
67
|
+
createError = 'Network error';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function startEdit(user: AuthUser) {
|
|
72
|
+
editingId = user.id;
|
|
73
|
+
editDisplayName = user.displayName;
|
|
74
|
+
editRole = user.role;
|
|
75
|
+
editPassword = '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function saveEdit() {
|
|
79
|
+
if (!editingId) return;
|
|
80
|
+
const patch: Record<string, unknown> = {
|
|
81
|
+
displayName: editDisplayName,
|
|
82
|
+
role: editRole,
|
|
83
|
+
};
|
|
84
|
+
if (editPassword.trim()) patch.password = editPassword;
|
|
85
|
+
try {
|
|
86
|
+
const res = await fetch(`/api/admin/users/${editingId}`, {
|
|
87
|
+
method: 'PUT',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
credentials: 'include',
|
|
90
|
+
body: JSON.stringify(patch),
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) return;
|
|
93
|
+
editingId = null;
|
|
94
|
+
await fetchUsers();
|
|
95
|
+
} catch { /* ignore */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function deleteUser(id: string) {
|
|
99
|
+
try {
|
|
100
|
+
await fetch(`/api/admin/users/${id}`, {
|
|
101
|
+
method: 'DELETE',
|
|
102
|
+
credentials: 'include',
|
|
103
|
+
});
|
|
104
|
+
await fetchUsers();
|
|
105
|
+
} catch { /* ignore */ }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fetchUsers();
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<div class="admin-users">
|
|
112
|
+
<div class="admin-users-header">
|
|
113
|
+
<h2>Users</h2>
|
|
114
|
+
<button type="button" class="admin-btn" onclick={() => { showCreate = !showCreate; }}>
|
|
115
|
+
{showCreate ? 'Cancel' : 'New user'}
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{#if showCreate}
|
|
120
|
+
<form class="admin-create-form" onsubmit={(e) => { e.preventDefault(); createUser(); }}>
|
|
121
|
+
<input class="admin-input" type="text" placeholder="Username" bind:value={newUsername} />
|
|
122
|
+
<input class="admin-input" type="text" placeholder="Display name" bind:value={newDisplayName} />
|
|
123
|
+
<input class="admin-input" type="password" placeholder="Password" bind:value={newPassword} />
|
|
124
|
+
<select class="admin-input" bind:value={newRole}>
|
|
125
|
+
<option value="user">User</option>
|
|
126
|
+
<option value="admin">Admin</option>
|
|
127
|
+
</select>
|
|
128
|
+
<button type="submit" class="admin-btn" disabled={!newUsername.trim() || !newPassword.trim()}>Create</button>
|
|
129
|
+
{#if createError}<div class="admin-error">{createError}</div>{/if}
|
|
130
|
+
</form>
|
|
131
|
+
{/if}
|
|
132
|
+
|
|
133
|
+
{#if loading}
|
|
134
|
+
<p class="admin-muted">Loading...</p>
|
|
135
|
+
{:else if error}
|
|
136
|
+
<p class="admin-error">{error}</p>
|
|
137
|
+
{:else}
|
|
138
|
+
<ul class="admin-user-list">
|
|
139
|
+
{#each users as user (user.id)}
|
|
140
|
+
<li class="admin-user-item">
|
|
141
|
+
{#if editingId === user.id}
|
|
142
|
+
<form class="admin-edit-form" onsubmit={(e) => { e.preventDefault(); saveEdit(); }}>
|
|
143
|
+
<input class="admin-input" type="text" bind:value={editDisplayName} />
|
|
144
|
+
<input class="admin-input" type="password" placeholder="New password (leave empty to keep)" bind:value={editPassword} />
|
|
145
|
+
<select class="admin-input" bind:value={editRole}>
|
|
146
|
+
<option value="user">User</option>
|
|
147
|
+
<option value="admin">Admin</option>
|
|
148
|
+
</select>
|
|
149
|
+
<div class="admin-edit-actions">
|
|
150
|
+
<button type="submit" class="admin-btn">Save</button>
|
|
151
|
+
<button type="button" class="admin-btn-secondary" onclick={() => { editingId = null; }}>Cancel</button>
|
|
152
|
+
</div>
|
|
153
|
+
</form>
|
|
154
|
+
{:else}
|
|
155
|
+
<div class="admin-user-info">
|
|
156
|
+
<span class="admin-user-name">{user.displayName}</span>
|
|
157
|
+
<span class="admin-user-meta">{user.username} · {user.role}</span>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="admin-user-actions">
|
|
160
|
+
<button type="button" class="admin-btn-secondary" onclick={() => startEdit(user)}>Edit</button>
|
|
161
|
+
<button type="button" class="admin-btn-danger" onclick={() => deleteUser(user.id)}>Delete</button>
|
|
162
|
+
</div>
|
|
163
|
+
{/if}
|
|
164
|
+
</li>
|
|
165
|
+
{/each}
|
|
166
|
+
</ul>
|
|
167
|
+
{/if}
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<style>
|
|
171
|
+
.admin-users { padding: 24px; font-family: system-ui, sans-serif; color: var(--shell-fg); }
|
|
172
|
+
.admin-users-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
|
173
|
+
.admin-users-header h2 { margin: 0; font-size: 18px; }
|
|
174
|
+
.admin-create-form, .admin-edit-form { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; max-width: 400px; }
|
|
175
|
+
.admin-input { padding: 8px 12px; background: var(--shell-bg, #1a1a2e); color: var(--shell-fg); border: 1px solid var(--shell-border, #3a3a5c); border-radius: var(--shell-radius, 6px); font-size: 13px; }
|
|
176
|
+
.admin-btn { padding: 6px 14px; background: var(--shell-accent, #7c7cf0); color: var(--shell-bg); border: none; border-radius: var(--shell-radius, 6px); font-weight: 600; cursor: pointer; font-size: 13px; }
|
|
177
|
+
.admin-btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
178
|
+
.admin-btn-secondary { padding: 6px 14px; background: transparent; color: var(--shell-fg-subtle); border: 1px solid var(--shell-border); border-radius: var(--shell-radius, 6px); cursor: pointer; font-size: 12px; }
|
|
179
|
+
.admin-btn-danger { padding: 6px 14px; background: transparent; color: var(--shell-error, #d32f2f); border: 1px solid var(--shell-error, #d32f2f); border-radius: var(--shell-radius, 6px); cursor: pointer; font-size: 12px; }
|
|
180
|
+
.admin-user-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
181
|
+
.admin-user-item { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: var(--shell-bg-elevated, #252540); border: 1px solid var(--shell-border, #3a3a5c); border-radius: var(--shell-radius, 6px); }
|
|
182
|
+
.admin-user-info { display: flex; flex-direction: column; gap: 2px; }
|
|
183
|
+
.admin-user-name { font-weight: 600; }
|
|
184
|
+
.admin-user-meta { font-size: 11px; color: var(--shell-fg-subtle); }
|
|
185
|
+
.admin-user-actions { display: flex; gap: 6px; }
|
|
186
|
+
.admin-edit-actions { display: flex; gap: 6px; }
|
|
187
|
+
.admin-error { color: var(--shell-error, #d32f2f); font-size: 13px; }
|
|
188
|
+
.admin-muted { color: var(--shell-fg-muted); font-style: italic; }
|
|
189
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Admin app — user management, auth settings, and system controls.
|
|
3
|
+
* Framework-shipped: registered in host.ts during bootstrap.
|
|
4
|
+
* Admin-gated via manifest flag (ADR-011).
|
|
5
|
+
*/
|
|
6
|
+
import type { App } from '../apps/types';
|
|
7
|
+
export declare const adminApp: App;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Admin app — user management, auth settings, and system controls.
|
|
3
|
+
* Framework-shipped: registered in host.ts during bootstrap.
|
|
4
|
+
* Admin-gated via manifest flag (ADR-011).
|
|
5
|
+
*/
|
|
6
|
+
export const adminApp = {
|
|
7
|
+
manifest: {
|
|
8
|
+
id: 'sh3-admin-app',
|
|
9
|
+
label: 'Admin',
|
|
10
|
+
version: '0.1.0',
|
|
11
|
+
requiredShards: ['sh3-admin'],
|
|
12
|
+
layoutVersion: 1,
|
|
13
|
+
admin: true,
|
|
14
|
+
},
|
|
15
|
+
initialLayout: {
|
|
16
|
+
type: 'tabs',
|
|
17
|
+
activeTab: 0,
|
|
18
|
+
tabs: [
|
|
19
|
+
{ slotId: 'admin.users', viewId: 'sh3-admin:users', label: 'Users' },
|
|
20
|
+
{ slotId: 'admin.auth', viewId: 'sh3-admin:auth', label: 'Auth' },
|
|
21
|
+
{ slotId: 'admin.system', viewId: 'sh3-admin:system', label: 'System' },
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Admin shard — framework-shipped shard for user management,
|
|
3
|
+
* auth settings, and system controls.
|
|
4
|
+
*
|
|
5
|
+
* Contributes three views:
|
|
6
|
+
* - `sh3-admin:users` — user CRUD
|
|
7
|
+
* - `sh3-admin:auth` — auth settings toggles
|
|
8
|
+
* - `sh3-admin:system` — restart + status
|
|
9
|
+
*
|
|
10
|
+
* `.svelte.ts` because mounting Svelte components requires rune access.
|
|
11
|
+
*/
|
|
12
|
+
import { mount, unmount } from 'svelte';
|
|
13
|
+
import UsersView from './UsersView.svelte';
|
|
14
|
+
import AuthSettingsView from './AuthSettingsView.svelte';
|
|
15
|
+
import SystemView from './SystemView.svelte';
|
|
16
|
+
/** Module-level server URL, set during activate. */
|
|
17
|
+
export let adminServerUrl = '';
|
|
18
|
+
export const adminShard = {
|
|
19
|
+
manifest: {
|
|
20
|
+
id: 'sh3-admin',
|
|
21
|
+
label: 'Admin',
|
|
22
|
+
version: '0.1.0',
|
|
23
|
+
views: [
|
|
24
|
+
{ id: 'sh3-admin:users', label: 'Users' },
|
|
25
|
+
{ id: 'sh3-admin:auth', label: 'Auth Settings' },
|
|
26
|
+
{ id: 'sh3-admin:system', label: 'System' },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
activate(ctx) {
|
|
30
|
+
const usersFactory = {
|
|
31
|
+
mount(container, _context) {
|
|
32
|
+
const instance = mount(UsersView, { target: container });
|
|
33
|
+
return { unmount() { unmount(instance); } };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
const authFactory = {
|
|
37
|
+
mount(container, _context) {
|
|
38
|
+
const instance = mount(AuthSettingsView, { target: container });
|
|
39
|
+
return { unmount() { unmount(instance); } };
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const systemFactory = {
|
|
43
|
+
mount(container, _context) {
|
|
44
|
+
const instance = mount(SystemView, { target: container });
|
|
45
|
+
return { unmount() { unmount(instance); } };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
ctx.registerView('sh3-admin:users', usersFactory);
|
|
49
|
+
ctx.registerView('sh3-admin:auth', authFactory);
|
|
50
|
+
ctx.registerView('sh3-admin:system', systemFactory);
|
|
51
|
+
},
|
|
52
|
+
};
|
package/dist/api.d.ts
CHANGED
|
@@ -16,7 +16,8 @@ export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, I
|
|
|
16
16
|
export type { ResolvedPackage } from './registry/client';
|
|
17
17
|
export { fetchRegistries, fetchBundle, buildPackageMeta } from './registry/client';
|
|
18
18
|
export { validateRegistryIndex } from './registry/schema';
|
|
19
|
-
export { isAdmin, getAuthHeader } from './auth/index';
|
|
19
|
+
export { isAdmin, isAuthenticated, isGuest, getUser, getAuthHeader } from './auth/index';
|
|
20
|
+
export type { AuthUser, AuthSession, BootConfig } from './auth/types';
|
|
20
21
|
/** Runtime feature flags for target-dependent behavior. */
|
|
21
22
|
export declare const capabilities: {
|
|
22
23
|
/** Whether this target supports hot-installing packages via dynamic import from blob URL. */
|
package/dist/api.js
CHANGED
|
@@ -38,7 +38,7 @@ export { registeredShards, activeShards } from './shards/activate.svelte';
|
|
|
38
38
|
export { fetchRegistries, fetchBundle, buildPackageMeta } from './registry/client';
|
|
39
39
|
export { validateRegistryIndex } from './registry/schema';
|
|
40
40
|
// Admin mode (framework-internal components read admin status).
|
|
41
|
-
export { isAdmin, getAuthHeader } from './auth/index';
|
|
41
|
+
export { isAdmin, isAuthenticated, isGuest, getUser, getAuthHeader } from './auth/index';
|
|
42
42
|
/** Runtime feature flags for target-dependent behavior. */
|
|
43
43
|
export const capabilities = {
|
|
44
44
|
/** Whether this target supports hot-installing packages via dynamic import from blob URL. */
|
package/dist/apps/lifecycle.d.ts
CHANGED
|
@@ -31,7 +31,12 @@ export declare function unloadApp(id: string): void;
|
|
|
31
31
|
* refcount hold intact, and its view containers stay alive in the pool.
|
|
32
32
|
* Launching the same app again is a root swap only.
|
|
33
33
|
*
|
|
34
|
+
* Fires `suspend` hooks on all required shards (in order), then on the
|
|
35
|
+
* app itself. Any hook returning `false` cancels navigation — the user
|
|
36
|
+
* stays in the app. Returns `true` if navigation succeeded, `false` if
|
|
37
|
+
* cancelled.
|
|
38
|
+
*
|
|
34
39
|
* Writes `null` to `__shell__:last-app` so reloading the page while on
|
|
35
40
|
* home lands on home, not on the formerly-active app.
|
|
36
41
|
*/
|
|
37
|
-
export declare function returnToHome():
|
|
42
|
+
export declare function returnToHome(): Promise<boolean>;
|
package/dist/apps/lifecycle.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* return-to-home (null). Boot reads it to decide whether to auto-launch.
|
|
13
13
|
*/
|
|
14
14
|
import { createStateZones } from '../state/zones.svelte';
|
|
15
|
-
import { activateShard, deactivateShard, registeredShards, } from '../shards/activate.svelte';
|
|
15
|
+
import { activateShard, deactivateShard, getShardContext, registeredShards, } from '../shards/activate.svelte';
|
|
16
16
|
import { attachApp, detachApp, switchToApp, switchToHome, } from '../layout/store.svelte';
|
|
17
17
|
import { activeApp, getRegisteredApp } from './registry.svelte';
|
|
18
18
|
import { createZoneManager } from '../state/manage';
|
|
@@ -69,7 +69,7 @@ function getOrCreateAppContext(appId) {
|
|
|
69
69
|
* @throws If the app is not registered or a required shard is not registered.
|
|
70
70
|
*/
|
|
71
71
|
export async function launchApp(id) {
|
|
72
|
-
var _a;
|
|
72
|
+
var _a, _b, _c;
|
|
73
73
|
const app = getRegisteredApp(id);
|
|
74
74
|
if (!app) {
|
|
75
75
|
throw new Error(`Cannot launch app "${id}": not registered`);
|
|
@@ -82,6 +82,14 @@ export async function launchApp(id) {
|
|
|
82
82
|
unloadApp(activeApp.id);
|
|
83
83
|
}
|
|
84
84
|
else if (activeApp.id === id) {
|
|
85
|
+
// Re-entering the same app from Home — fire resume hooks.
|
|
86
|
+
for (const shardId of app.manifest.requiredShards) {
|
|
87
|
+
const shard = registeredShards.get(shardId);
|
|
88
|
+
const shardCtx = getShardContext(shardId);
|
|
89
|
+
if (shard && shardCtx)
|
|
90
|
+
void ((_a = shard.resume) === null || _a === void 0 ? void 0 : _a.call(shard, shardCtx));
|
|
91
|
+
}
|
|
92
|
+
void ((_b = app.resume) === null || _b === void 0 ? void 0 : _b.call(app, getOrCreateAppContext(id)));
|
|
85
93
|
switchToApp();
|
|
86
94
|
writeLastApp(id);
|
|
87
95
|
return;
|
|
@@ -99,7 +107,7 @@ export async function launchApp(id) {
|
|
|
99
107
|
// Attach the layout (creates the workspace-zone proxy with version
|
|
100
108
|
// gate) and run the app's optional activate hook.
|
|
101
109
|
attachApp(app);
|
|
102
|
-
void ((
|
|
110
|
+
void ((_c = app.activate) === null || _c === void 0 ? void 0 : _c.call(app, getOrCreateAppContext(id)));
|
|
103
111
|
activeApp.id = id;
|
|
104
112
|
switchToApp();
|
|
105
113
|
writeLastApp(id);
|
|
@@ -151,10 +159,26 @@ export function unloadApp(id) {
|
|
|
151
159
|
* refcount hold intact, and its view containers stay alive in the pool.
|
|
152
160
|
* Launching the same app again is a root swap only.
|
|
153
161
|
*
|
|
162
|
+
* Fires `suspend` hooks on all required shards (in order), then on the
|
|
163
|
+
* app itself. Any hook returning `false` cancels navigation — the user
|
|
164
|
+
* stays in the app. Returns `true` if navigation succeeded, `false` if
|
|
165
|
+
* cancelled.
|
|
166
|
+
*
|
|
154
167
|
* Writes `null` to `__shell__:last-app` so reloading the page while on
|
|
155
168
|
* home lands on home, not on the formerly-active app.
|
|
156
169
|
*/
|
|
157
|
-
export function returnToHome() {
|
|
170
|
+
export async function returnToHome() {
|
|
171
|
+
const app = activeApp.id ? getRegisteredApp(activeApp.id) : null;
|
|
172
|
+
if (app) {
|
|
173
|
+
for (const shardId of app.manifest.requiredShards) {
|
|
174
|
+
const shard = registeredShards.get(shardId);
|
|
175
|
+
if ((shard === null || shard === void 0 ? void 0 : shard.suspend) && (await shard.suspend()) === false)
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
if (app.suspend && (await app.suspend()) === false)
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
158
181
|
switchToHome();
|
|
159
182
|
writeLastApp(null);
|
|
183
|
+
return true;
|
|
160
184
|
}
|
|
@@ -14,8 +14,11 @@ export declare const activeApp: {
|
|
|
14
14
|
id: string | null;
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
|
-
* Register an app with the framework.
|
|
18
|
-
*
|
|
17
|
+
* Register (or re-register) an app with the framework.
|
|
18
|
+
*
|
|
19
|
+
* If an app with the same id already exists it is silently replaced,
|
|
20
|
+
* which is the expected path during package updates — the new bundle is
|
|
21
|
+
* loaded and re-registered without requiring a full page reload.
|
|
19
22
|
*
|
|
20
23
|
* @param app - The app module to register.
|
|
21
24
|
*/
|
|
@@ -21,17 +21,16 @@ export const registeredApps = $state(new Map());
|
|
|
21
21
|
*/
|
|
22
22
|
export const activeApp = $state({ id: null });
|
|
23
23
|
/**
|
|
24
|
-
* Register an app with the framework.
|
|
25
|
-
*
|
|
24
|
+
* Register (or re-register) an app with the framework.
|
|
25
|
+
*
|
|
26
|
+
* If an app with the same id already exists it is silently replaced,
|
|
27
|
+
* which is the expected path during package updates — the new bundle is
|
|
28
|
+
* loaded and re-registered without requiring a full page reload.
|
|
26
29
|
*
|
|
27
30
|
* @param app - The app module to register.
|
|
28
31
|
*/
|
|
29
32
|
export function registerApp(app) {
|
|
30
|
-
|
|
31
|
-
if (registeredApps.has(id)) {
|
|
32
|
-
throw new Error(`App "${id}" is already registered`);
|
|
33
|
-
}
|
|
34
|
-
registeredApps.set(id, app);
|
|
33
|
+
registeredApps.set(app.manifest.id, app);
|
|
35
34
|
}
|
|
36
35
|
/**
|
|
37
36
|
* Reactive snapshot of all registered app manifests. Shell home iterates
|