clay-server 5.1.0-beta.2 → 5.1.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.
Files changed (101) hide show
  1. package/bin/cli.js +9 -91
  2. package/lib/builtin-mates.js +7 -7
  3. package/lib/daemon-settings.js +11 -0
  4. package/lib/daemon.js +8 -37
  5. package/lib/linear-store.js +35 -8
  6. package/lib/mate-identity-file.js +39 -0
  7. package/lib/mate-knowledge-sync.js +1 -1
  8. package/lib/mate-ready-creation.js +2 -1
  9. package/lib/mates-identity.js +4 -4
  10. package/lib/mates-prompts.js +4 -3
  11. package/lib/mates.js +12 -11
  12. package/lib/os-users.js +5 -16
  13. package/lib/project-connection.js +0 -7
  14. package/lib/project-debate.js +3 -2
  15. package/lib/project-filesystem.js +21 -39
  16. package/lib/project-mate-interaction.js +7 -6
  17. package/lib/project-server-settings.js +31 -0
  18. package/lib/project-sessions.js +57 -184
  19. package/lib/project.js +3 -3
  20. package/lib/public/app.js +6 -37
  21. package/lib/public/css/input.css +3 -1
  22. package/lib/public/css/menus.css +3 -0
  23. package/lib/public/css/overlays.css +7 -0
  24. package/lib/public/css/project-settings-modal.css +56 -0
  25. package/lib/public/css/server-settings-system.css +125 -0
  26. package/lib/public/css/session-github.css +25 -16
  27. package/lib/public/css/settings-ui.css +247 -22
  28. package/lib/public/css/shared-browser.css +7 -3
  29. package/lib/public/css/title-bar.css +5 -1
  30. package/lib/public/index.html +173 -447
  31. package/lib/public/modules/account-island.js +9 -7
  32. package/lib/public/modules/admin-settings-email.js +197 -0
  33. package/lib/public/modules/admin-settings-invitations.js +183 -0
  34. package/lib/public/modules/admin-settings-projects.js +198 -0
  35. package/lib/public/modules/admin-settings-ui.js +100 -0
  36. package/lib/public/modules/admin-settings-users.js +331 -0
  37. package/lib/public/modules/admin.js +19 -949
  38. package/lib/public/modules/app-dm.js +0 -2
  39. package/lib/public/modules/app-messages.js +13 -52
  40. package/lib/public/modules/app-rate-limit.js +70 -12
  41. package/lib/public/modules/avatar.js +6 -1
  42. package/lib/public/modules/context-sources.js +27 -18
  43. package/lib/public/modules/default-ai.js +14 -7
  44. package/lib/public/modules/header-tui-font.js +10 -20
  45. package/lib/public/modules/home-mate-avatar-editor.js +2 -2
  46. package/lib/public/modules/linear-settings.js +3 -2
  47. package/lib/public/modules/markdown.js +5 -6
  48. package/lib/public/modules/notification-settings.js +81 -0
  49. package/lib/public/modules/notifications.js +44 -28
  50. package/lib/public/modules/personal-settings-dialog.js +51 -0
  51. package/lib/public/modules/profile.js +15 -6
  52. package/lib/public/modules/project-settings-environment.js +106 -0
  53. package/lib/public/modules/project-settings.js +72 -628
  54. package/lib/public/modules/server-settings-controls.js +316 -0
  55. package/lib/public/modules/server-settings-environment.js +194 -0
  56. package/lib/public/modules/server-settings-navigation.js +39 -0
  57. package/lib/public/modules/server-settings-state.js +54 -0
  58. package/lib/public/modules/server-settings.js +116 -788
  59. package/lib/public/modules/server-storage-settings.js +84 -0
  60. package/lib/public/modules/session-github.js +2 -1
  61. package/lib/public/modules/session-presence-ui.js +1 -1
  62. package/lib/public/modules/settings-env-values.js +34 -0
  63. package/lib/public/modules/settings-select.js +121 -0
  64. package/lib/public/modules/shared-browser.js +22 -12
  65. package/lib/public/modules/stt-picker.js +14 -5
  66. package/lib/public/modules/terminal-prefs.js +22 -0
  67. package/lib/public/modules/theme.js +88 -52
  68. package/lib/public/modules/user-settings-account.js +318 -0
  69. package/lib/public/modules/user-settings-appearance.js +55 -0
  70. package/lib/public/modules/user-settings-editors.js +24 -0
  71. package/lib/public/modules/user-settings-integrations.js +104 -0
  72. package/lib/public/modules/user-settings-layout.js +80 -0
  73. package/lib/public/modules/user-settings-preferences.js +48 -0
  74. package/lib/public/modules/user-settings-profile.js +53 -0
  75. package/lib/public/modules/user-settings-themes.js +38 -0
  76. package/lib/public/modules/user-settings-voice.js +75 -0
  77. package/lib/public/modules/user-settings.js +53 -480
  78. package/lib/public/style.css +4 -0
  79. package/lib/sdk-message-processor.js +16 -4
  80. package/lib/sdk-quota-snapshot.js +37 -0
  81. package/lib/server-appearance-settings.js +71 -0
  82. package/lib/server-linear.js +14 -8
  83. package/lib/server-mate-instructions.js +2 -2
  84. package/lib/server-mates.js +2 -1
  85. package/lib/server-notification-settings.js +67 -0
  86. package/lib/server-settings.js +4 -0
  87. package/lib/server-storage.js +57 -0
  88. package/lib/server.js +10 -2
  89. package/lib/sessions.js +3 -4
  90. package/lib/startup-projects.js +40 -0
  91. package/lib/storage-native-walk.js +85 -0
  92. package/lib/storage-usage.js +144 -0
  93. package/lib/users-preferences.js +0 -74
  94. package/lib/users.js +0 -4
  95. package/lib/ws-schema.js +2 -4
  96. package/lib/yoke/acp-process-manager.js +2 -2
  97. package/lib/yoke/adapters/codex.js +12 -52
  98. package/lib/yoke/codex-app-server.js +2 -2
  99. package/lib/yoke/codex-model-catalog.js +52 -0
  100. package/lib/yoke/codex-rate-limits.js +26 -0
  101. package/package.json +3 -3
@@ -1,806 +1,134 @@
1
- // server-settings.js — Full-screen server settings overlay
1
+ import { initStorageUsage, loadStorageUsage } from './server-storage-settings.js';
2
+ // Full-app server administration shell. Page state survives navigation and closing.
3
+ import { store } from './store.js';
4
+ import { getWs } from './ws-ref.js';
2
5
  import { refreshIcons } from './icons.js';
3
- import { showToast, copyToClipboard } from './utils.js';
4
- import { parseEnvString, looksLikeEnv, serializeEnvValue } from './project-settings.js';
5
- import { checkAdminAccess, loadAdminSection } from './admin.js';
6
6
  import { closeFileViewer } from './filebrowser.js';
7
+ import { checkAdminAccess, loadAdminSection } from './admin.js';
8
+ import { openUserSettings } from './user-settings.js';
7
9
  import { initNetworkSettings, loadNetworkSettings } from './server-network-settings.js';
8
-
9
- var ctx = null;
10
- var settingsEl = null;
11
- var settingsBtn = null;
12
- var closeBtn = null;
13
- var navItems = null;
14
- var sections = null;
15
- var statsTimer = null;
16
-
17
- export function initServerSettings(appCtx) {
18
- ctx = appCtx;
19
- settingsEl = document.getElementById("server-settings");
20
- settingsBtn = document.getElementById("server-settings-btn");
21
- closeBtn = document.getElementById("server-settings-close");
22
-
23
- if (!settingsEl || !settingsBtn) return;
24
-
25
- navItems = settingsEl.querySelectorAll(".settings-nav-item");
26
- sections = settingsEl.querySelectorAll(".server-settings-section");
27
-
28
- initNetworkSettings();
29
-
30
- // Open settings
31
- settingsBtn.addEventListener("click", function () {
32
- openSettings();
33
- });
34
-
35
- // Close settings
36
- closeBtn.addEventListener("click", function () {
37
- closeSettings();
38
- });
39
-
40
- // ESC to close
41
- document.addEventListener("keydown", function (e) {
42
- if (e.key === "Escape" && !settingsEl.classList.contains("hidden")) {
43
- closeSettings();
44
- }
45
- });
46
-
47
- // Nav item clicks
48
- for (var i = 0; i < navItems.length; i++) {
49
- navItems[i].addEventListener("click", function () {
50
- var section = this.dataset.section;
51
- switchSection(section);
52
- });
53
- }
54
-
55
- // Mobile dropdown nav
56
- var navDropdown = document.getElementById("settings-nav-dropdown");
57
- if (navDropdown) {
58
- navDropdown.addEventListener("change", function () {
59
- switchSection(this.value);
60
- });
61
- }
62
-
63
- // Copyable command blocks
64
- var copyables = settingsEl.querySelectorAll(".settings-copyable");
65
- for (var c = 0; c < copyables.length; c++) {
66
- copyables[c].addEventListener("click", function () {
67
- var text = this.dataset.copy;
68
- if (!text) return;
69
- var btn = this.querySelector(".settings-copy-btn");
70
- copyToClipboard(text).then(function () {
71
- if (btn) {
72
- var orig = btn.textContent;
73
- btn.textContent = "✓";
74
- setTimeout(function () { btn.textContent = orig; }, 1500);
75
- }
76
- showToast("Copied to clipboard");
77
- });
78
- });
79
- }
80
-
81
- // Notification toggles
82
- var notifAlert = document.getElementById("settings-notif-alert");
83
- var notifSound = document.getElementById("settings-notif-sound");
84
- var notifPush = document.getElementById("settings-notif-push");
85
-
86
- if (notifAlert) {
87
- notifAlert.addEventListener("change", function () {
88
- var src = document.getElementById("notif-toggle-alert");
89
- if (src) {
90
- src.checked = this.checked;
91
- src.dispatchEvent(new Event("change", { bubbles: true }));
92
- }
93
- });
94
- }
95
-
96
- if (notifSound) {
97
- notifSound.addEventListener("change", function () {
98
- var src = document.getElementById("notif-toggle-sound");
99
- if (src) {
100
- src.checked = this.checked;
101
- src.dispatchEvent(new Event("change", { bubbles: true }));
102
- }
103
- });
104
- }
105
-
106
- if (notifPush) {
107
- notifPush.addEventListener("change", function () {
108
- var src = document.getElementById("notif-toggle-push");
109
- if (src) {
110
- src.checked = this.checked;
111
- src.dispatchEvent(new Event("change", { bubbles: true }));
112
- }
113
- });
114
- }
115
-
116
- // PIN buttons
117
- var pinSetBtn = document.getElementById("settings-pin-set-btn");
118
- var pinRemoveBtn = document.getElementById("settings-pin-remove-btn");
119
- var pinSaveBtn = document.getElementById("settings-pin-save-btn");
120
- var pinCancelBtn = document.getElementById("settings-pin-cancel-btn");
121
- var pinInput = document.getElementById("settings-pin-input");
122
-
123
- if (pinSetBtn) pinSetBtn.addEventListener("click", function () { showPinForm(); });
124
- if (pinRemoveBtn) pinRemoveBtn.addEventListener("click", function () {
125
- var ws = ctx.ws;
126
- if (ws && ws.readyState === 1) {
127
- ws.send(JSON.stringify({ type: "set_pin", pin: null }));
128
- }
129
- });
130
- if (pinSaveBtn) pinSaveBtn.addEventListener("click", function () { submitPin(); });
131
- if (pinCancelBtn) pinCancelBtn.addEventListener("click", function () { hidePinForm(); });
132
- if (pinInput) pinInput.addEventListener("keydown", function (e) {
133
- if (e.key === "Enter") { e.preventDefault(); submitPin(); }
134
- if (e.key === "Escape") { e.preventDefault(); hidePinForm(); }
10
+ import { initServerControls } from './server-settings-controls.js';
11
+ import { initServerEnvironment, loadSharedEnv } from './server-settings-environment.js';
12
+ import { renderSettingsNavigation } from './server-settings-navigation.js';
13
+ import { hasSettingsDrafts, confirmSettingsAction, trapSettingsFocus } from './server-settings-state.js';
14
+ import { enhanceSettingsSelects } from './settings-select.js';
15
+ export { updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleServerSettingResult } from './server-settings-controls.js';
16
+ export { handleSharedEnv, handleSharedEnvSaved } from './server-settings-environment.js';
17
+
18
+ function isolateSettings(active) {
19
+ if (active) {
20
+ var entries = Array.from(document.body.children).filter(function (el) { return el !== root() && el.id !== 'confirm-modal' && el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE'; }).map(function (el) { var entry = { el: el, inert: el.inert, hidden: el.getAttribute('aria-hidden') }; el.inert = true; el.setAttribute('aria-hidden', 'true'); return entry; });
21
+ store.set({ serverSettingsInert: entries });
22
+ } else {
23
+ (store.get('serverSettingsInert') || []).forEach(function (entry) { entry.el.inert = entry.inert; if (entry.hidden === null) entry.el.removeAttribute('aria-hidden'); else entry.el.setAttribute('aria-hidden', entry.hidden); });
24
+ store.set({ serverSettingsInert: [] });
25
+ }
26
+ }
27
+ function root() { return document.getElementById('server-settings'); }
28
+ export function isSettingsOpen() { return root() && !root().classList.contains('hidden'); }
29
+ function send(type) { var ws = getWs(); if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: type })); }
30
+
31
+ function selectPage(id, focus) {
32
+ if (!store.get('serverSettingsAuthorized')) return;
33
+ if (id === 'admin-invites') id = 'admin-users';
34
+ var nav = document.getElementById('settings-page-nav');
35
+ if (!nav.querySelector('[data-section="' + id + '"]')) id = 'overview';
36
+ store.set({ serverSettingsPage: id });
37
+ root().querySelectorAll('.server-settings-section').forEach(function (page) { page.classList.toggle('active', page.dataset.section === id); });
38
+ nav.querySelectorAll('button').forEach(function (button) {
39
+ button.classList.toggle('active', button.dataset.section === id);
40
+ if (button.dataset.section === id) button.setAttribute('aria-current', 'page'); else button.removeAttribute('aria-current');
135
41
  });
136
-
137
- // Auto-continue moved to User Settings > Behavior
138
-
139
- // Keep awake toggle
140
- var keepAwakeToggle = document.getElementById("settings-keep-awake");
141
- if (keepAwakeToggle) {
142
- keepAwakeToggle.addEventListener("change", function () {
143
- var ws = ctx.ws;
144
- if (ws && ws.readyState === 1) {
145
- ws.send(JSON.stringify({ type: "set_keep_awake", value: this.checked }));
146
- }
147
- });
148
- }
149
-
150
- // Inherit supplementary groups toggle (OS-users mode)
151
- var inheritGroupsToggle = document.getElementById("settings-inherit-groups");
152
- if (inheritGroupsToggle) {
153
- inheritGroupsToggle.addEventListener("change", function () {
154
- var ws = ctx.ws;
155
- if (ws && ws.readyState === 1) {
156
- ws.send(JSON.stringify({ type: "set_inherit_groups", value: this.checked }));
157
- }
158
- });
159
- }
160
-
161
- // Image retention select
162
- var imageRetentionSelect = document.getElementById("settings-image-retention");
163
- if (imageRetentionSelect) {
164
- imageRetentionSelect.addEventListener("change", function () {
165
- var ws = ctx.ws;
166
- if (ws && ws.readyState === 1) {
167
- ws.send(JSON.stringify({ type: "set_image_retention", days: parseInt(this.value, 10) }));
168
- }
169
- });
170
- }
171
-
172
- // Global CLAUDE.md: save button
173
- var ssClaudeMdSave = document.getElementById("ss-claudemd-save");
174
- if (ssClaudeMdSave) {
175
- ssClaudeMdSave.addEventListener("click", function () { saveGlobalClaudeMd(); });
176
- }
177
-
178
- // Shared environment: add button
179
- var ssEnvAddBtn = document.getElementById("ss-env-add-btn");
180
- if (ssEnvAddBtn) {
181
- ssEnvAddBtn.addEventListener("click", function () {
182
- addSharedEnvRow("", "", true);
183
- autoSaveSharedEnv();
184
- });
185
- }
186
-
187
- // Restart server
188
- var restartBtn = document.getElementById("settings-restart-btn");
189
- if (restartBtn) {
190
- restartBtn.addEventListener("click", function () {
191
- var ws = ctx.ws;
192
- if (ws && ws.readyState === 1) {
193
- restartBtn.disabled = true;
194
- restartBtn.textContent = "Restarting...";
195
- ws.send(JSON.stringify({ type: "restart_server" }));
196
- }
197
- });
198
- }
199
-
200
- // Shutdown server
201
- var shutdownInput = document.getElementById("settings-shutdown-input");
202
- var shutdownBtn = document.getElementById("settings-shutdown-btn");
203
-
204
- if (shutdownInput && shutdownBtn) {
205
- shutdownInput.addEventListener("input", function () {
206
- var val = this.value.trim().toLowerCase();
207
- shutdownBtn.disabled = val !== "shutdown";
208
- });
209
-
210
- shutdownInput.addEventListener("keydown", function (e) {
211
- if (e.key === "Enter") {
212
- e.preventDefault();
213
- if (!shutdownBtn.disabled) shutdownBtn.click();
214
- }
215
- });
216
-
217
- shutdownBtn.addEventListener("click", function () {
218
- var val = shutdownInput.value.trim().toLowerCase();
219
- if (val !== "shutdown") return;
220
- var ws = ctx.ws;
221
- if (ws && ws.readyState === 1) {
222
- shutdownBtn.disabled = true;
223
- shutdownBtn.textContent = "Shutting down...";
224
- shutdownInput.disabled = true;
225
- ws.send(JSON.stringify({ type: "shutdown_server" }));
226
- }
227
- });
228
- }
229
- }
230
-
231
- function switchSection(sectionName) {
232
- for (var i = 0; i < navItems.length; i++) {
233
- var isActive = navItems[i].dataset.section === sectionName;
234
- navItems[i].classList.toggle("active", isActive);
235
- }
236
- // Sync mobile dropdown
237
- var navDropdown = document.getElementById("settings-nav-dropdown");
238
- if (navDropdown && navDropdown.value !== sectionName) {
239
- navDropdown.value = sectionName;
42
+ document.getElementById('settings-nav-dropdown').value = id;
43
+ if (id === 'network') loadNetworkSettings();
44
+ if (id === 'storage') loadStorageUsage();
45
+ if (id === 'environment') loadSharedEnv();
46
+ if (id.indexOf('admin-') === 0) {
47
+ var body = document.getElementById(id + '-body');
48
+ if (id !== 'admin-smtp' || !(store.get('serverSettingsDrafts') || {}).smtp) loadAdminSection(id, body);
240
49
  }
241
- for (var j = 0; j < sections.length; j++) {
242
- var isActive2 = sections[j].dataset.section === sectionName;
243
- sections[j].classList.toggle("active", isActive2);
244
- }
245
-
246
- if (sectionName === "network") loadNetworkSettings();
247
-
248
- // Lazy-load section data
249
- if (sectionName === "claudemd") loadGlobalClaudeMd();
250
- if (sectionName === "environment") loadSharedEnv();
251
- if (sectionName === "admin-users" || sectionName === "admin-invites" || sectionName === "admin-projects" || sectionName === "admin-smtp") {
252
- var adminBody = document.getElementById(sectionName + "-body");
253
- if (adminBody) loadAdminSection(sectionName, adminBody);
50
+ if (focus) {
51
+ var heading = root().querySelector('.server-settings-section.active h2');
52
+ heading.tabIndex = -1; heading.focus();
254
53
  }
255
54
  }
256
55
 
257
- function openSettings() {
56
+ async function openSettings() {
57
+ if (isSettingsOpen()) return;
258
58
  closeFileViewer();
259
- settingsEl.classList.remove("hidden");
260
- settingsBtn.classList.add("active");
261
- refreshIcons(settingsEl);
262
- populateSettings();
263
- if (settingsEl.querySelector('.server-settings-section.active[data-section="network"]')) loadNetworkSettings();
264
- requestDaemonConfig();
265
- resetRestartButton();
266
- resetShutdownForm();
267
-
268
- // Show/hide admin sections based on role
269
- checkAdminAccess().then(function (isAdmin) {
270
- var adminEls = settingsEl.querySelectorAll(".settings-admin-only");
271
- for (var ai = 0; ai < adminEls.length; ai++) {
272
- adminEls[ai].style.display = isAdmin ? "" : "none";
59
+ isolateSettings(true);
60
+ store.set({ serverSettingsReturnFocus: document.activeElement });
61
+ root().classList.remove('hidden');
62
+ document.getElementById('server-settings-btn').classList.add('active');
63
+ document.getElementById('server-settings-close').focus();
64
+ var status = document.getElementById('settings-access-status');
65
+ status.textContent = 'Checking administrator access…';
66
+ store.set({ serverSettingsAuthorized: false });
67
+ root().querySelectorAll('.server-settings-section').forEach(function (page) { page.classList.remove('active'); });
68
+ try {
69
+ var response = await fetch('/api/me', { credentials: 'same-origin' });
70
+ if (!response.ok) throw new Error('Could not verify access. Close and reopen Settings to retry.');
71
+ var data = await response.json();
72
+ if (data.multiUser && (!data.user || data.user.role !== 'admin')) {
73
+ status.textContent = 'Server settings require an administrator. Your personal preferences are available below.';
74
+ document.getElementById('settings-page-nav').replaceChildren();
75
+ document.getElementById('settings-nav-dropdown').replaceChildren();
76
+ return;
273
77
  }
274
- });
275
-
276
- // Start periodic stats refresh
277
- requestStats();
278
- statsTimer = setInterval(requestStats, 5000);
279
- }
280
-
281
- function resetRestartButton() {
282
- var btn = document.getElementById("settings-restart-btn");
283
- var errorEl = document.getElementById("settings-restart-error");
284
- if (btn) { btn.disabled = false; btn.innerHTML = '<i data-lucide="refresh-cw" style="width:14px;height:14px;vertical-align:-2px;margin-right:4px;"></i>Restart'; }
285
- if (errorEl) errorEl.classList.add("hidden");
286
- }
287
-
288
- function resetShutdownForm() {
289
- var input = document.getElementById("settings-shutdown-input");
290
- var btn = document.getElementById("settings-shutdown-btn");
291
- var errorEl = document.getElementById("settings-shutdown-error");
292
- if (input) { input.value = ""; input.disabled = false; }
293
- if (btn) { btn.disabled = true; btn.textContent = "Shutdown"; }
294
- if (errorEl) errorEl.classList.add("hidden");
78
+ await checkAdminAccess();
79
+ if (!isSettingsOpen()) return;
80
+ store.set({ serverSettingsAuthorized: true });
81
+ status.textContent = '';
82
+ renderSettingsNavigation(!!data.multiUser, selectPage);
83
+ selectPage(store.get('serverSettingsPage') || 'overview', false);
84
+ if (!hasSettingsDrafts()) send('get_daemon_config');
85
+ setTimeout(function () {
86
+ if (isSettingsOpen() && !store.get('serverSettingsConfigLoaded')) status.textContent = 'Server configuration has not loaded. Reconnect, then reopen Settings to retry.';
87
+ }, 12000);
88
+ send('process_stats');
89
+ clearInterval(store.get('serverSettingsStatsTimer'));
90
+ store.set({ serverSettingsStatsTimer: setInterval(function () { send('process_stats'); }, 5000) });
91
+ document.getElementById('settings-project-slug').textContent = store.get('currentSlug') || '(none)';
92
+ document.getElementById('settings-ws-path').textContent = store.get('wsPath') || '/ws';
93
+ var pill = document.getElementById('skip-perms-pill');
94
+ document.getElementById('settings-skip-perms').textContent = pill && !pill.classList.contains('hidden') ? 'Skip' : 'Permission checks';
95
+ enhanceSettingsSelects(root()); refreshIcons(root());
96
+ } catch (error) { status.textContent = error.message; }
295
97
  }
296
98
 
297
99
  function closeSettings() {
298
- settingsEl.classList.add("hidden");
299
- settingsBtn.classList.remove("active");
300
- if (statsTimer) {
301
- clearInterval(statsTimer);
302
- statsTimer = null;
303
- }
304
- }
305
-
306
- export function isSettingsOpen() {
307
- return settingsEl && !settingsEl.classList.contains("hidden");
308
- }
309
-
310
- function requestStats() {
311
- var ws = ctx.ws;
312
- if (ws && ws.readyState === 1) {
313
- ws.send(JSON.stringify({ type: "process_stats" }));
314
- }
315
- }
316
-
317
- function populateSettings() {
318
- var nameEl = document.getElementById("settings-server-name");
319
- var versionEl = document.getElementById("settings-server-version");
320
- var slugEl = document.getElementById("settings-project-slug");
321
- var wsPathEl = document.getElementById("settings-ws-path");
322
- var skipPermsEl = document.getElementById("settings-skip-perms");
323
-
324
- // Nav header defaults to hostname (updated by updateDaemonConfig)
325
- if (nameEl && !nameEl.textContent) nameEl.textContent = "Server";
326
-
327
- // Version is set from WebSocket "info" message in app.js
328
- if (versionEl && !versionEl.textContent) versionEl.textContent = "-";
329
-
330
- if (slugEl) slugEl.textContent = ctx.currentSlug || "(default)";
331
- if (wsPathEl) wsPathEl.textContent = ctx.wsPath || "/ws";
332
-
333
- // Skip permissions
334
- var spBanner = document.getElementById("skip-perms-pill");
335
- if (skipPermsEl) {
336
- var isSkip = spBanner && !spBanner.classList.contains("hidden");
337
- skipPermsEl.textContent = isSkip ? "Enabled" : "Disabled";
338
- skipPermsEl.classList.toggle("settings-badge-on", isSkip);
339
- }
340
-
341
- // Sync notification toggles
342
- syncNotifToggles();
343
-
344
- }
345
-
346
- function syncNotifToggles() {
347
- var pairs = [
348
- ["notif-toggle-alert", "settings-notif-alert"],
349
- ["notif-toggle-sound", "settings-notif-sound"],
350
- ["notif-toggle-push", "settings-notif-push"],
351
- ];
352
- for (var i = 0; i < pairs.length; i++) {
353
- var src = document.getElementById(pairs[i][0]);
354
- var dst = document.getElementById(pairs[i][1]);
355
- if (src && dst) dst.checked = src.checked;
356
- }
100
+ function close() {
101
+ isolateSettings(false);
102
+ root().classList.add('hidden');
103
+ document.getElementById('server-settings-btn').classList.remove('active');
104
+ clearInterval(store.get('serverSettingsStatsTimer'));
105
+ var previous = store.get('serverSettingsReturnFocus');
106
+ if (previous && previous.isConnected) previous.focus();
107
+ }
108
+ if (hasSettingsDrafts()) confirmSettingsAction('You have unsaved changes. Closing keeps these drafts in this tab, but reloading the page will lose them.', 'Close with drafts', close);
109
+ else close();
110
+ }
111
+
112
+ export function initServerSettings() {
113
+ if (!root()) return;
114
+ initNetworkSettings(); initServerControls(); initServerEnvironment(); initStorageUsage();
115
+ document.getElementById('server-settings-btn').addEventListener('click', openSettings);
116
+ document.getElementById('server-settings-close').onclick = closeSettings;
117
+ document.getElementById('settings-personal-link').onclick = function () {
118
+ function openPersonal() { isolateSettings(false); document.getElementById('server-settings-btn').classList.remove('active'); root().classList.add('hidden'); clearInterval(store.get('serverSettingsStatsTimer')); openUserSettings('us-notifications'); }
119
+ if (hasSettingsDrafts()) confirmSettingsAction('Your server settings drafts will remain in this tab while you visit Personal settings.', 'Open Personal settings', openPersonal);
120
+ else openPersonal();
121
+ };
122
+ document.addEventListener('keydown', function (event) {
123
+ if (!isSettingsOpen() || document.querySelector('.admin-modal-overlay, #settings-confirm, .confirm-above-settings')) return;
124
+ if (event.key === 'Escape') { event.preventDefault(); closeSettings(); }
125
+ trapSettingsFocus(event, root());
126
+ });
127
+ new MutationObserver(function () { enhanceSettingsSelects(root()); }).observe(root(), { childList: true, subtree: true });
357
128
  }
358
129
 
359
130
  export function updateSettingsStats(data) {
360
131
  if (!isSettingsOpen()) return;
361
- var pid = document.getElementById("settings-status-pid");
362
- var uptime = document.getElementById("settings-status-uptime");
363
- var rss = document.getElementById("settings-status-rss");
364
- var sessions = document.getElementById("settings-status-sessions");
365
- var clients = document.getElementById("settings-status-clients");
366
-
367
- if (pid) pid.textContent = String(data.pid);
368
- if (uptime) uptime.textContent = formatUptime(data.uptime);
369
- if (rss) rss.textContent = formatBytes(data.memory.rss);
370
- if (sessions) sessions.textContent = String(data.sessions);
371
- if (clients) clients.textContent = String(data.clients);
372
- }
373
-
374
- // --- Daemon config ---
375
- function requestDaemonConfig() {
376
- var ws = ctx.ws;
377
- if (ws && ws.readyState === 1) {
378
- ws.send(JSON.stringify({ type: "get_daemon_config" }));
379
- }
380
- }
381
-
382
- export function updateDaemonConfig(config) {
383
- // Nav header: show hostname (strip .local suffix, lowercase)
384
- var nameEl = document.getElementById("settings-server-name");
385
- if (nameEl && config.hostname) {
386
- var displayHost = config.hostname.replace(/\.local$/i, "").toLowerCase();
387
- nameEl.textContent = displayHost;
388
- nameEl.title = config.hostname;
389
- }
390
-
391
- // Host
392
- var hostnameEl = document.getElementById("settings-hostname");
393
- var lanIpEl = document.getElementById("settings-lan-ip");
394
- if (hostnameEl) hostnameEl.textContent = config.hostname || "-";
395
- if (lanIpEl) lanIpEl.textContent = config.lanIp || "";
396
-
397
- // Port
398
- var portEl = document.getElementById("settings-port");
399
- if (portEl) portEl.textContent = String(config.port || "-");
400
-
401
- // TLS
402
- var tlsEl = document.getElementById("settings-tls");
403
- if (tlsEl) {
404
- tlsEl.textContent = config.tls ? "Enabled" : "Disabled";
405
- tlsEl.classList.toggle("settings-badge-green", !!config.tls);
406
- }
407
-
408
- // Debug
409
- var debugEl = document.getElementById("settings-debug");
410
- if (debugEl) {
411
- debugEl.textContent = config.debug ? "Enabled" : "Disabled";
412
- debugEl.classList.toggle("settings-badge-on", !!config.debug);
413
- }
414
-
415
- // PIN status
416
- updatePinStatus(!!config.pinEnabled);
417
-
418
- // Auto-continue on rate limit
419
- // Auto-continue is now per-user (User Settings > Behavior)
420
-
421
- // Keep awake
422
- var keepAwakeToggle = document.getElementById("settings-keep-awake");
423
- if (keepAwakeToggle) keepAwakeToggle.checked = !!config.keepAwake;
424
-
425
- // Inherit supplementary groups (only relevant in OS-users mode)
426
- var inheritGroupsToggle = document.getElementById("settings-inherit-groups");
427
- if (inheritGroupsToggle) inheritGroupsToggle.checked = config.inheritGroups !== false;
428
-
429
- // Image retention
430
- var imageRetentionSelect = document.getElementById("settings-image-retention");
431
- if (imageRetentionSelect && config.imageRetentionDays !== undefined) {
432
- imageRetentionSelect.value = String(config.imageRetentionDays);
433
- }
434
-
435
- // Early Access toggle
436
- var channelToggle = document.getElementById("settings-update-channel");
437
- if (channelToggle) {
438
- channelToggle.checked = (config.updateChannel === "beta");
439
- channelToggle.onchange = function () {
440
- var channel = channelToggle.checked ? "beta" : "stable";
441
- if (ctx.ws && ctx.ws.readyState === 1) {
442
- ctx.ws.send(JSON.stringify({ type: "set_update_channel", channel: channel }));
443
- // Auto-trigger update check after channel change
444
- setTimeout(function () {
445
- ctx.ws.send(JSON.stringify({ type: "check_update" }));
446
- }, 200);
447
- }
448
- };
449
- }
450
-
451
- // Show keep awake section/nav only on macOS
452
- var keepAwakeSection = document.getElementById("settings-keep-awake-section");
453
- var keepAwakeNav = document.getElementById("settings-keep-awake-nav");
454
- var keepAwakeOpt = document.getElementById("settings-keep-awake-opt");
455
- if (config.platform === "darwin") {
456
- if (keepAwakeSection) keepAwakeSection.classList.remove("hidden");
457
- if (keepAwakeNav) keepAwakeNav.classList.remove("hidden");
458
- if (keepAwakeOpt) keepAwakeOpt.classList.remove("hidden");
459
- } else {
460
- if (keepAwakeSection) keepAwakeSection.classList.add("hidden");
461
- if (keepAwakeNav) keepAwakeNav.classList.add("hidden");
462
- if (keepAwakeOpt) keepAwakeOpt.classList.add("hidden");
463
- }
464
-
465
- // Show inherit-groups section/nav only in OS-users mode
466
- var inheritGroupsSection = document.getElementById("settings-inherit-groups-section");
467
- var inheritGroupsNav = document.getElementById("settings-inherit-groups-nav");
468
- var inheritGroupsOpt = document.getElementById("settings-inherit-groups-opt");
469
- if (config.osUsers) {
470
- if (inheritGroupsSection) inheritGroupsSection.classList.remove("hidden");
471
- if (inheritGroupsNav) inheritGroupsNav.classList.remove("hidden");
472
- if (inheritGroupsOpt) inheritGroupsOpt.classList.remove("hidden");
473
- } else {
474
- if (inheritGroupsSection) inheritGroupsSection.classList.add("hidden");
475
- if (inheritGroupsNav) inheritGroupsNav.classList.add("hidden");
476
- if (inheritGroupsOpt) inheritGroupsOpt.classList.add("hidden");
477
- }
478
- }
479
-
480
- export function handleSetPinResult(msg) {
481
- if (msg.ok) {
482
- updatePinStatus(!!msg.pinEnabled);
483
- hidePinForm();
484
- showToast(msg.pinEnabled ? "PIN set successfully" : "PIN removed");
485
- }
486
- }
487
-
488
- export function handleKeepAwakeChanged(msg) {
489
- var keepAwakeToggle = document.getElementById("settings-keep-awake");
490
- if (keepAwakeToggle) keepAwakeToggle.checked = !!msg.keepAwake;
491
- }
492
-
493
- export function handleInheritGroupsChanged(msg) {
494
- var inheritGroupsToggle = document.getElementById("settings-inherit-groups");
495
- if (inheritGroupsToggle) inheritGroupsToggle.checked = msg.inheritGroups !== false;
496
- }
497
-
498
- export function handleAutoContinueChanged(msg) {
499
- // Auto-continue is now per-user; server broadcast no longer updates UI
500
- }
501
-
502
- export function handleRestartResult(msg) {
503
- var restartBtn = document.getElementById("settings-restart-btn");
504
- var errorEl = document.getElementById("settings-restart-error");
505
-
506
- if (msg.ok) {
507
- if (restartBtn) restartBtn.textContent = "Server restarting...";
508
- showToast("Server is restarting...");
509
- } else {
510
- if (restartBtn) {
511
- restartBtn.textContent = "Restart";
512
- restartBtn.disabled = false;
513
- }
514
- if (errorEl) {
515
- errorEl.textContent = msg.error || "Restart failed";
516
- errorEl.classList.remove("hidden");
517
- }
518
- }
519
- }
520
-
521
- export function handleShutdownResult(msg) {
522
- var shutdownInput = document.getElementById("settings-shutdown-input");
523
- var shutdownBtn = document.getElementById("settings-shutdown-btn");
524
- var errorEl = document.getElementById("settings-shutdown-error");
525
-
526
- if (msg.ok) {
527
- if (shutdownBtn) shutdownBtn.textContent = "Server stopped";
528
- showToast("Server is shutting down...");
529
- } else {
530
- if (shutdownBtn) {
531
- shutdownBtn.textContent = "Shutdown";
532
- shutdownBtn.disabled = false;
533
- }
534
- if (shutdownInput) shutdownInput.disabled = false;
535
- if (errorEl) {
536
- errorEl.textContent = msg.error || "Shutdown failed";
537
- errorEl.classList.remove("hidden");
538
- }
539
- }
540
- }
541
-
542
- // --- PIN form management ---
543
- function showPinForm() {
544
- var form = document.getElementById("settings-pin-form");
545
- var input = document.getElementById("settings-pin-input");
546
- var errorEl = document.getElementById("settings-pin-error");
547
- if (form) form.classList.remove("hidden");
548
- if (errorEl) errorEl.classList.add("hidden");
549
- if (input) { input.value = ""; input.focus(); }
550
- }
551
-
552
- function hidePinForm() {
553
- var form = document.getElementById("settings-pin-form");
554
- var input = document.getElementById("settings-pin-input");
555
- var errorEl = document.getElementById("settings-pin-error");
556
- if (form) form.classList.add("hidden");
557
- if (input) input.value = "";
558
- if (errorEl) errorEl.classList.add("hidden");
559
- }
560
-
561
- function submitPin() {
562
- var input = document.getElementById("settings-pin-input");
563
- var errorEl = document.getElementById("settings-pin-error");
564
- if (!input) return;
565
- var pin = input.value.trim();
566
- if (!/^\d{6}$/.test(pin)) {
567
- if (errorEl) errorEl.classList.remove("hidden");
568
- input.focus();
569
- return;
570
- }
571
- if (errorEl) errorEl.classList.add("hidden");
572
- var ws = ctx.ws;
573
- if (ws && ws.readyState === 1) {
574
- ws.send(JSON.stringify({ type: "set_pin", pin: pin }));
575
- }
576
- }
577
-
578
- function updatePinStatus(enabled) {
579
- var statusEl = document.getElementById("settings-pin-status");
580
- var setBtn = document.getElementById("settings-pin-set-btn");
581
- var removeBtn = document.getElementById("settings-pin-remove-btn");
582
- var actionLabel = document.getElementById("settings-pin-action-label");
583
-
584
- if (statusEl) {
585
- statusEl.textContent = enabled ? "Enabled" : "Disabled";
586
- statusEl.classList.toggle("settings-badge-green", enabled);
587
- }
588
- if (setBtn) setBtn.textContent = enabled ? "Change PIN" : "Set PIN";
589
- if (removeBtn) removeBtn.classList.toggle("hidden", !enabled);
590
- if (actionLabel) actionLabel.textContent = enabled ? "Change PIN" : "Set PIN";
591
- }
592
-
593
- // ===== Global CLAUDE.md =====
594
- function loadGlobalClaudeMd() {
595
- var editor = document.getElementById("ss-claudemd-editor");
596
- var status = document.getElementById("ss-claudemd-status");
597
- var saveStatus = document.getElementById("ss-claudemd-save-status");
598
- if (saveStatus) saveStatus.textContent = "";
599
- if (status) status.textContent = "Loading...";
600
-
601
- var ws = ctx.ws;
602
- if (ws && ws.readyState === 1) {
603
- ws.send(JSON.stringify({ type: "read_global_claude_md" }));
604
- }
605
- }
606
-
607
- export function handleGlobalClaudeMdRead(msg) {
608
- var editor = document.getElementById("ss-claudemd-editor");
609
- var status = document.getElementById("ss-claudemd-status");
610
- if (!editor) return;
611
-
612
- if (msg.error) {
613
- editor.value = "";
614
- if (status) status.textContent = "No global CLAUDE.md found. Save to create one.";
615
- } else {
616
- editor.value = msg.content || "";
617
- if (status) status.textContent = "";
618
- }
619
- }
620
-
621
- function saveGlobalClaudeMd() {
622
- var editor = document.getElementById("ss-claudemd-editor");
623
- var saveStatus = document.getElementById("ss-claudemd-save-status");
624
- if (!editor) return;
625
-
626
- var ws = ctx.ws;
627
- if (ws && ws.readyState === 1) {
628
- ws.send(JSON.stringify({ type: "write_global_claude_md", content: editor.value }));
629
- if (saveStatus) saveStatus.textContent = "Saving...";
630
- }
631
- }
632
-
633
- export function handleGlobalClaudeMdWrite(msg) {
634
- var saveStatus = document.getElementById("ss-claudemd-save-status");
635
- if (!saveStatus) return;
636
- if (msg.ok) {
637
- saveStatus.textContent = "Saved";
638
- setTimeout(function () { saveStatus.textContent = ""; }, 2000);
639
- } else {
640
- saveStatus.textContent = "Error: " + (msg.error || "Failed to save");
641
- }
642
- }
643
-
644
- // ===== Shared Environment Variables =====
645
- var sharedEnvSaveTimer = null;
646
- var sharedEnvStatusTimer = null;
647
-
648
- function showSharedEnvSaveStatus(text, duration) {
649
- var saveStatus = document.getElementById("ss-env-save-status");
650
- if (!saveStatus) return;
651
- if (sharedEnvStatusTimer) clearTimeout(sharedEnvStatusTimer);
652
- saveStatus.textContent = text;
653
- sharedEnvStatusTimer = setTimeout(function () { saveStatus.textContent = ""; }, duration);
654
- }
655
-
656
- function loadSharedEnv() {
657
- var saveStatus = document.getElementById("ss-env-save-status");
658
- if (saveStatus) saveStatus.textContent = "";
659
-
660
- var ws = ctx.ws;
661
- if (ws && ws.readyState === 1) {
662
- ws.send(JSON.stringify({ type: "get_shared_env" }));
663
- }
664
- }
665
-
666
- export function handleSharedEnv(msg) {
667
- var list = document.getElementById("ss-env-list");
668
- if (!list) return;
669
- list.innerHTML = "";
670
-
671
- var pairs = parseEnvString(msg.envrc || "");
672
- for (var i = 0; i < pairs.length; i++) {
673
- addSharedEnvRow(pairs[i].key, pairs[i].value, false);
674
- }
675
- refreshIcons();
676
- }
677
-
678
- export function handleSharedEnvSaved(msg) {
679
- if (msg.ok) {
680
- showSharedEnvSaveStatus(msg.timing || "Saved", 5000);
681
- } else {
682
- showSharedEnvSaveStatus("Error: " + (msg.error || "Failed to save"), 5000);
683
- }
684
- }
685
-
686
- function buildSharedEnvString() {
687
- var list = document.getElementById("ss-env-list");
688
- if (!list) return "";
689
- var rows = list.querySelectorAll(".ps-env-row");
690
- var lines = [];
691
- for (var i = 0; i < rows.length; i++) {
692
- var keyInput = rows[i].querySelector(".ps-env-key");
693
- var valInput = rows[i].querySelector(".ps-env-val");
694
- var key = keyInput ? keyInput.value.trim() : "";
695
- var val = valInput ? valInput.value : "";
696
- if (key) lines.push("export " + key + "=" + serializeEnvValue(val));
697
- }
698
- return lines.join("\n");
699
- }
700
-
701
- function addSharedEnvRow(key, value, focus) {
702
- var list = document.getElementById("ss-env-list");
703
- if (!list) return;
704
-
705
- var row = document.createElement("div");
706
- row.className = "ps-env-row";
707
-
708
- var keyInput = document.createElement("input");
709
- keyInput.type = "text";
710
- keyInput.className = "ps-env-key";
711
- keyInput.placeholder = "KEY";
712
- keyInput.value = key;
713
- keyInput.spellcheck = false;
714
- keyInput.autocomplete = "off";
715
-
716
- var valInput = document.createElement("input");
717
- valInput.type = "text";
718
- valInput.className = "ps-env-val";
719
- valInput.placeholder = "value";
720
- valInput.value = value;
721
- valInput.spellcheck = false;
722
- valInput.autocomplete = "off";
723
-
724
- var delBtn = document.createElement("button");
725
- delBtn.className = "ps-env-del";
726
- delBtn.title = "Remove";
727
- delBtn.innerHTML = '<i data-lucide="x"></i>';
728
-
729
- delBtn.addEventListener("click", function () {
730
- row.remove();
731
- autoSaveSharedEnv();
732
- });
733
-
734
- keyInput.addEventListener("input", function () { autoSaveSharedEnv(); });
735
- valInput.addEventListener("input", function () { autoSaveSharedEnv(); });
736
-
737
- // Paste detection
738
- keyInput.addEventListener("paste", function (e) {
739
- var text = (e.clipboardData || window.clipboardData).getData("text");
740
- if (text && looksLikeEnv(text)) {
741
- e.preventDefault();
742
- var pairs = parseEnvString(text);
743
- if (pairs.length > 0) {
744
- keyInput.value = pairs[0].key;
745
- valInput.value = pairs[0].value;
746
- for (var p = 1; p < pairs.length; p++) {
747
- addSharedEnvRow(pairs[p].key, pairs[p].value, false);
748
- }
749
- autoSaveSharedEnv();
750
- }
751
- }
752
- });
753
-
754
- valInput.addEventListener("paste", function (e) {
755
- var text = (e.clipboardData || window.clipboardData).getData("text");
756
- if (text && text.indexOf("\n") !== -1 && text.indexOf("=") !== -1) {
757
- e.preventDefault();
758
- var pairs = parseEnvString(text);
759
- if (pairs.length > 0) {
760
- keyInput.value = pairs[0].key;
761
- valInput.value = pairs[0].value;
762
- for (var p = 1; p < pairs.length; p++) {
763
- addSharedEnvRow(pairs[p].key, pairs[p].value, false);
764
- }
765
- autoSaveSharedEnv();
766
- }
767
- }
768
- });
769
-
770
- row.appendChild(keyInput);
771
- row.appendChild(valInput);
772
- row.appendChild(delBtn);
773
- list.appendChild(row);
774
- refreshIcons();
775
-
776
- if (focus) keyInput.focus();
777
- }
778
-
779
- function autoSaveSharedEnv() {
780
- if (sharedEnvSaveTimer) clearTimeout(sharedEnvSaveTimer);
781
- sharedEnvSaveTimer = setTimeout(function () {
782
- var envrc = buildSharedEnvString();
783
- var ws = ctx.ws;
784
- if (ws && ws.readyState === 1) {
785
- ws.send(JSON.stringify({ type: "set_shared_env", envrc: envrc }));
786
- showSharedEnvSaveStatus("Saving...", 2000);
787
- }
788
- }, 800);
789
- }
790
-
791
- function formatBytes(n) {
792
- if (n >= 1073741824) return (n / 1073741824).toFixed(1) + " GB";
793
- if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
794
- if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
795
- return n + " B";
796
- }
797
-
798
- function formatUptime(seconds) {
799
- var d = Math.floor(seconds / 86400);
800
- var h = Math.floor((seconds % 86400) / 3600);
801
- var m = Math.floor((seconds % 3600) / 60);
802
- var s = Math.floor(seconds % 60);
803
- if (d > 0) return d + "d " + h + "h " + m + "m";
804
- if (h > 0) return h + "h " + m + "m " + s + "s";
805
- return m + "m " + s + "s";
132
+ var values = { pid: data.pid, uptime: Math.floor(data.uptime / 3600) + 'h ' + Math.floor(data.uptime % 3600 / 60) + 'm', rss: (data.memory.rss / 1048576).toFixed(1) + ' MB', sessions: data.sessions, clients: data.clients };
133
+ Object.keys(values).forEach(function (key) { var el = document.getElementById('settings-status-' + key); if (el) el.textContent = String(values[key]); });
806
134
  }