clay-server 5.1.0-beta.2 → 6.0.0-beta.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.
- package/bin/cli.js +9 -91
- package/lib/daemon-settings.js +11 -0
- package/lib/daemon.js +8 -37
- package/lib/linear-store.js +35 -8
- package/lib/os-users.js +5 -16
- package/lib/project-connection.js +0 -7
- package/lib/project-filesystem.js +21 -39
- package/lib/project-mate-interaction.js +2 -2
- package/lib/project-server-settings.js +31 -0
- package/lib/project-sessions.js +57 -184
- package/lib/public/app.js +6 -37
- package/lib/public/css/input.css +3 -1
- package/lib/public/css/project-settings-modal.css +56 -0
- package/lib/public/css/server-settings-system.css +125 -0
- package/lib/public/css/session-github.css +25 -16
- package/lib/public/css/settings-ui.css +247 -22
- package/lib/public/css/shared-browser.css +2 -0
- package/lib/public/css/title-bar.css +5 -1
- package/lib/public/index.html +170 -446
- package/lib/public/modules/account-island.js +9 -7
- package/lib/public/modules/admin-settings-email.js +197 -0
- package/lib/public/modules/admin-settings-invitations.js +183 -0
- package/lib/public/modules/admin-settings-projects.js +198 -0
- package/lib/public/modules/admin-settings-ui.js +100 -0
- package/lib/public/modules/admin-settings-users.js +331 -0
- package/lib/public/modules/admin.js +19 -949
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-messages.js +13 -52
- package/lib/public/modules/app-rate-limit.js +70 -12
- package/lib/public/modules/context-sources.js +27 -18
- package/lib/public/modules/default-ai.js +14 -7
- package/lib/public/modules/header-tui-font.js +10 -20
- package/lib/public/modules/linear-settings.js +3 -2
- package/lib/public/modules/markdown.js +5 -6
- package/lib/public/modules/notification-settings.js +81 -0
- package/lib/public/modules/notifications.js +44 -28
- package/lib/public/modules/personal-settings-dialog.js +51 -0
- package/lib/public/modules/profile.js +15 -6
- package/lib/public/modules/project-settings-environment.js +106 -0
- package/lib/public/modules/project-settings.js +72 -628
- package/lib/public/modules/server-settings-controls.js +316 -0
- package/lib/public/modules/server-settings-environment.js +194 -0
- package/lib/public/modules/server-settings-navigation.js +39 -0
- package/lib/public/modules/server-settings-state.js +54 -0
- package/lib/public/modules/server-settings.js +116 -788
- package/lib/public/modules/server-storage-settings.js +84 -0
- package/lib/public/modules/session-github.js +2 -1
- package/lib/public/modules/session-presence-ui.js +1 -1
- package/lib/public/modules/settings-env-values.js +34 -0
- package/lib/public/modules/settings-select.js +121 -0
- package/lib/public/modules/shared-browser.js +10 -7
- package/lib/public/modules/stt-picker.js +14 -5
- package/lib/public/modules/terminal-prefs.js +22 -0
- package/lib/public/modules/theme.js +88 -52
- package/lib/public/modules/user-settings-account.js +318 -0
- package/lib/public/modules/user-settings-appearance.js +55 -0
- package/lib/public/modules/user-settings-editors.js +24 -0
- package/lib/public/modules/user-settings-integrations.js +104 -0
- package/lib/public/modules/user-settings-layout.js +80 -0
- package/lib/public/modules/user-settings-preferences.js +48 -0
- package/lib/public/modules/user-settings-profile.js +53 -0
- package/lib/public/modules/user-settings-themes.js +38 -0
- package/lib/public/modules/user-settings-voice.js +75 -0
- package/lib/public/modules/user-settings.js +53 -480
- package/lib/public/style.css +4 -0
- package/lib/sdk-message-processor.js +16 -4
- package/lib/sdk-quota-snapshot.js +37 -0
- package/lib/server-appearance-settings.js +71 -0
- package/lib/server-linear.js +14 -8
- package/lib/server-notification-settings.js +67 -0
- package/lib/server-settings.js +4 -0
- package/lib/server-storage.js +56 -0
- package/lib/server.js +10 -2
- package/lib/sessions.js +2 -3
- package/lib/startup-projects.js +40 -0
- package/lib/storage-usage.js +127 -0
- package/lib/users-preferences.js +0 -74
- package/lib/users.js +0 -4
- package/lib/ws-schema.js +2 -4
- package/lib/yoke/adapters/codex.js +8 -45
- package/lib/yoke/codex-model-catalog.js +40 -0
- package/lib/yoke/codex-rate-limits.js +26 -0
- package/package.json +3 -3
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { showToast } from './utils.js';
|
|
4
|
+
import { confirmSettingsAction, clearSettingsDraft, setSettingsDraft } from './server-settings-state.js';
|
|
5
|
+
import { enhanceSettingsSelects } from './settings-select.js';
|
|
6
|
+
|
|
7
|
+
function status(id, text) {
|
|
8
|
+
var el = document.getElementById(id);
|
|
9
|
+
if (el) { el.textContent = text; el.classList.remove('hidden'); }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sendSetting(type, values, buttonId, statusId) {
|
|
13
|
+
var ws = getWs();
|
|
14
|
+
if (!ws || ws.readyState !== 1) { status(statusId, 'Disconnected. Your change has not been saved.'); return; }
|
|
15
|
+
var button = document.getElementById(buttonId);
|
|
16
|
+
if (button) button.disabled = true;
|
|
17
|
+
var input = buttonId === 'settings-pin-save-btn' ? document.getElementById('settings-pin-input') : buttonId.endsWith('-save') ? document.getElementById(buttonId.slice(0, -5)) : null;
|
|
18
|
+
if (input) { input.disabled = true; input.dispatchEvent(new Event('settings-sync')); }
|
|
19
|
+
status(statusId, 'Saving…');
|
|
20
|
+
var pending = Object.assign({}, store.get('serverSettingsPending') || {});
|
|
21
|
+
pending[type] = { buttonId: buttonId, statusId: statusId };
|
|
22
|
+
store.set({ serverSettingsPending: pending });
|
|
23
|
+
ws.send(JSON.stringify(Object.assign({ type: type }, values)));
|
|
24
|
+
setTimeout(function () {
|
|
25
|
+
if ((store.get('serverSettingsPending') || {})[type] !== pending[type]) return;
|
|
26
|
+
if (button) button.disabled = false;
|
|
27
|
+
if (input) { input.disabled = false; input.dispatchEvent(new Event('settings-sync')); }
|
|
28
|
+
status(statusId, 'No acknowledgement received. Reconnect and retry; the server may have saved this value.');
|
|
29
|
+
}, 12000);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function handleServerSettingResult(msg) {
|
|
33
|
+
var type = msg.type.replace(/_result$/, '');
|
|
34
|
+
var pending = Object.assign({}, store.get('serverSettingsPending') || {});
|
|
35
|
+
var request = pending[type];
|
|
36
|
+
if (!request) return;
|
|
37
|
+
delete pending[type];
|
|
38
|
+
store.set({ serverSettingsPending: pending });
|
|
39
|
+
var button = document.getElementById(request.buttonId);
|
|
40
|
+
if (button) { button.disabled = false; if (msg.ok && request.buttonId.endsWith('-save')) button.hidden = true; }
|
|
41
|
+
var input = request.buttonId === 'settings-pin-save-btn' ? document.getElementById('settings-pin-input') : request.buttonId.endsWith('-save') ? document.getElementById(request.buttonId.slice(0, -5)) : null;
|
|
42
|
+
if (input) { input.disabled = false; input.dispatchEvent(new Event('settings-sync')); }
|
|
43
|
+
status(request.statusId, msg.ok ? 'Saved. ' + (msg.timing || '') : (msg.error || 'Could not save. Retry your change.'));
|
|
44
|
+
if (msg.ok) clearSettingsDraft(type);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function finishSetting(msg, id, field) {
|
|
48
|
+
handleServerSettingResult(msg);
|
|
49
|
+
var el = document.getElementById(id);
|
|
50
|
+
var key = 'set_keep_awake';
|
|
51
|
+
if (!msg.type.endsWith('_result') && (store.get('serverSettingsDrafts') || {})[key]) return;
|
|
52
|
+
if (el && msg.ok !== false && msg[field] !== undefined) el.checked = !!msg[field];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function updatePlatformControls(config) {
|
|
56
|
+
store.set({ serverSettingsConfig: config });
|
|
57
|
+
document.getElementById('settings-keep-awake').disabled = config.platform !== 'darwin';
|
|
58
|
+
status('settings-awake-support', config.platform === 'darwin' ? 'Applies while Clay is running on this Mac.' : 'Unavailable on this platform. Keep awake requires macOS.');
|
|
59
|
+
var channel = document.getElementById('settings-update-channel');
|
|
60
|
+
if (channel && !(store.get('serverSettingsDrafts') || {}).set_update_channel) channel.checked = config.updateChannel === 'beta';
|
|
61
|
+
enhanceSettingsSelects(document.getElementById('server-settings'));
|
|
62
|
+
document.getElementById('settings-image-retention').dispatchEvent(new Event('settings-sync'));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function initServerControls() {
|
|
66
|
+
['settings-keep-awake', 'settings-image-retention', 'settings-update-channel', 'settings-pin-set-btn', 'settings-pin-remove-btn'].forEach(function (id) { document.getElementById(id).disabled = true; });
|
|
67
|
+
var check = document.getElementById('settings-update-check');
|
|
68
|
+
var update = document.createElement('button'); update.type = 'button'; update.className = 'settings-btn-sm'; update.id = 'settings-install-update'; update.hidden = true; update.textContent = 'Update and restart…';
|
|
69
|
+
check.insertAdjacentElement('afterend', update);
|
|
70
|
+
var receipt = document.createElement('p'); receipt.id = 'settings-update-status'; receipt.className = 'settings-hint'; receipt.setAttribute('role', 'status'); update.insertAdjacentElement('afterend', receipt);
|
|
71
|
+
update.onclick = function () {
|
|
72
|
+
confirmSettingsAction('Install the available release and restart Clay? Active work will be interrupted for everyone. Unsaved settings remain drafts in this tab.', 'Update and restart', function () {
|
|
73
|
+
var ws = getWs();
|
|
74
|
+
if (!ws || ws.readyState !== 1) { status('settings-update-status', 'Disconnected. Reconnect before updating.'); return; }
|
|
75
|
+
update.disabled = true; status('settings-update-status', 'Update requested. Waiting for the daemon…');
|
|
76
|
+
ws.send(JSON.stringify({ type: 'update_now' }));
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
document.getElementById('settings-pin-set-btn').onclick = showPinForm;
|
|
81
|
+
document.getElementById('settings-pin-cancel-btn').onclick = hidePinForm;
|
|
82
|
+
document.getElementById('settings-pin-save-btn').onclick = submitPin;
|
|
83
|
+
document.getElementById('settings-pin-input').oninput = function () { setSettingsDraft('pin', !!this.value); };
|
|
84
|
+
document.getElementById('settings-pin-remove-btn').onclick = function () {
|
|
85
|
+
confirmSettingsAction('Remove the server PIN? This changes sign-in protection for new connections.', 'Remove PIN', function () {
|
|
86
|
+
sendSetting('set_pin', { pin: null }, 'settings-pin-remove-btn', 'settings-pin-error');
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
[
|
|
90
|
+
['settings-keep-awake', 'set_keep_awake', 'value', 'Saved changes apply now.'],
|
|
91
|
+
['settings-image-retention', 'set_image_retention', 'days', 'Review retention. The next cleanup permanently deletes expired images.'],
|
|
92
|
+
['settings-update-channel', 'set_update_channel', 'channel', 'Choose whether future update checks include prereleases.']
|
|
93
|
+
].forEach(function (entry) {
|
|
94
|
+
var input = document.getElementById(entry[0]);
|
|
95
|
+
var receipt = document.createElement('p');
|
|
96
|
+
receipt.id = entry[0] + '-status'; receipt.className = 'settings-hint'; receipt.setAttribute('role', 'status');
|
|
97
|
+
input.closest('.settings-toggle-row').insertAdjacentElement('afterend', receipt);
|
|
98
|
+
var save = document.createElement('button'); save.type = 'button'; save.className = 'settings-btn-sm'; save.textContent = 'Save changes';
|
|
99
|
+
save.id = entry[0] + '-save'; save.hidden = true; receipt.insertAdjacentElement('beforebegin', save);
|
|
100
|
+
input.onchange = function () {
|
|
101
|
+
setSettingsDraft(entry[1], true); save.hidden = false; receipt.textContent = entry[3];
|
|
102
|
+
};
|
|
103
|
+
save.onclick = function () {
|
|
104
|
+
var values = {}; values[entry[2]] = entry[2] === 'days' ? Number(input.value) : entry[2] === 'channel' ? (input.checked ? 'beta' : 'stable') : input.checked;
|
|
105
|
+
sendSetting(entry[1], values, save.id, receipt.id);
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
document.getElementById('settings-restart-btn').onclick = function () {
|
|
109
|
+
confirmSettingsAction('Restart Clay for everyone? Active agent work will be interrupted. Clients will reconnect. Unsaved settings are not applied.', 'Restart server', function () {
|
|
110
|
+
sendSetting('restart_server', {}, 'settings-restart-btn', 'settings-restart-error');
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
var shutdown = document.getElementById('settings-shutdown-input');
|
|
114
|
+
var shutdownButton = document.getElementById('settings-shutdown-btn');
|
|
115
|
+
var reviewShutdown = document.getElementById('settings-shutdown-review');
|
|
116
|
+
var shutdownFlow = document.getElementById('settings-shutdown-flow');
|
|
117
|
+
reviewShutdown.onclick = function () {
|
|
118
|
+
store.set({ serverShutdownReview: true });
|
|
119
|
+
reviewShutdown.hidden = true;
|
|
120
|
+
reviewShutdown.setAttribute('aria-expanded', 'true');
|
|
121
|
+
shutdownFlow.hidden = false;
|
|
122
|
+
shutdown.focus();
|
|
123
|
+
};
|
|
124
|
+
document.getElementById('settings-shutdown-back').onclick = function () {
|
|
125
|
+
store.set({ serverShutdownReview: false });
|
|
126
|
+
shutdownFlow.hidden = true;
|
|
127
|
+
reviewShutdown.hidden = false;
|
|
128
|
+
reviewShutdown.setAttribute('aria-expanded', 'false');
|
|
129
|
+
shutdown.value = '';
|
|
130
|
+
shutdownButton.disabled = true;
|
|
131
|
+
reviewShutdown.focus();
|
|
132
|
+
};
|
|
133
|
+
shutdown.oninput = function () { shutdownButton.disabled = this.value.trim().toLowerCase() !== 'shutdown'; };
|
|
134
|
+
shutdownButton.onclick = function () {
|
|
135
|
+
if (!store.get('serverShutdownReview') || shutdown.value.trim().toLowerCase() !== 'shutdown') return;
|
|
136
|
+
confirmSettingsAction('Stop Clay for everyone? Active work stops and all clients disconnect. Start the service again from the host to recover.', 'Shut down server', function () {
|
|
137
|
+
sendSetting('shutdown_server', {}, shutdownButton.id, 'settings-shutdown-error');
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function updateDaemonConfig(config) {
|
|
143
|
+
if (config.error) { status('settings-access-status', config.error); return; }
|
|
144
|
+
store.set({ serverSettingsConfigLoaded: true });
|
|
145
|
+
['settings-image-retention', 'settings-update-channel', 'settings-pin-set-btn', 'settings-pin-remove-btn'].forEach(function (id) { document.getElementById(id).disabled = false; });
|
|
146
|
+
// Nav header: show hostname (strip .local suffix, lowercase)
|
|
147
|
+
var nameEl = document.getElementById("settings-server-name");
|
|
148
|
+
if (nameEl && config.hostname) {
|
|
149
|
+
var displayHost = config.hostname.replace(/\.local$/i, "").toLowerCase();
|
|
150
|
+
nameEl.textContent = displayHost;
|
|
151
|
+
nameEl.title = config.hostname;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Host
|
|
155
|
+
var hostnameEl = document.getElementById("settings-hostname");
|
|
156
|
+
var lanIpEl = document.getElementById("settings-lan-ip");
|
|
157
|
+
if (hostnameEl) hostnameEl.textContent = config.hostname || "-";
|
|
158
|
+
if (lanIpEl) lanIpEl.textContent = config.lanIp || "";
|
|
159
|
+
|
|
160
|
+
// Port
|
|
161
|
+
var portEl = document.getElementById("settings-port");
|
|
162
|
+
if (portEl) portEl.textContent = String(config.port || "-");
|
|
163
|
+
|
|
164
|
+
// TLS
|
|
165
|
+
var tlsEl = document.getElementById("settings-tls");
|
|
166
|
+
if (tlsEl) {
|
|
167
|
+
tlsEl.textContent = config.tls ? "Enabled" : "Disabled";
|
|
168
|
+
tlsEl.classList.toggle("settings-badge-green", !!config.tls);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Debug
|
|
172
|
+
var debugEl = document.getElementById("settings-debug");
|
|
173
|
+
if (debugEl) {
|
|
174
|
+
debugEl.textContent = config.debug ? "Enabled" : "Disabled";
|
|
175
|
+
debugEl.classList.toggle("settings-badge-on", !!config.debug);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// PIN status
|
|
179
|
+
updatePinStatus(!!config.pinEnabled);
|
|
180
|
+
|
|
181
|
+
// Auto-continue on rate limit
|
|
182
|
+
// Auto-continue is now per-user (User Settings > Behavior)
|
|
183
|
+
|
|
184
|
+
// Keep awake
|
|
185
|
+
var keepAwakeToggle = document.getElementById("settings-keep-awake");
|
|
186
|
+
if (keepAwakeToggle && !(store.get('serverSettingsDrafts') || {}).set_keep_awake) keepAwakeToggle.checked = !!config.keepAwake;
|
|
187
|
+
|
|
188
|
+
// Image retention
|
|
189
|
+
var imageRetentionSelect = document.getElementById("settings-image-retention");
|
|
190
|
+
if (imageRetentionSelect && !(store.get('serverSettingsDrafts') || {}).set_image_retention && config.imageRetentionDays !== undefined) {
|
|
191
|
+
imageRetentionSelect.value = String(config.imageRetentionDays);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
updatePlatformControls(config);
|
|
195
|
+
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function handleSetPinResult(msg) {
|
|
199
|
+
if (msg.ok) {
|
|
200
|
+
updatePinStatus(!!msg.pinEnabled);
|
|
201
|
+
hidePinForm();
|
|
202
|
+
showToast(msg.pinEnabled ? "PIN saved" : "PIN removed");
|
|
203
|
+
clearSettingsDraft("pin");
|
|
204
|
+
} else { status("settings-pin-error", msg.error || "Could not save PIN. Your draft is retained."); }
|
|
205
|
+
document.getElementById("settings-pin-save-btn").disabled = false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function handleKeepAwakeChanged(msg) {
|
|
209
|
+
finishSetting(msg, "settings-keep-awake", "keepAwake");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function handleAutoContinueChanged(msg) {
|
|
213
|
+
// Auto-continue is now per-user; server broadcast no longer updates UI
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function handleRestartResult(msg) {
|
|
217
|
+
var restartBtn = document.getElementById("settings-restart-btn");
|
|
218
|
+
var errorEl = document.getElementById("settings-restart-error");
|
|
219
|
+
|
|
220
|
+
if (msg.ok) {
|
|
221
|
+
if (restartBtn) { restartBtn.textContent = "Server restarting..."; restartBtn.disabled = true; }
|
|
222
|
+
showToast("Server is restarting...");
|
|
223
|
+
} else {
|
|
224
|
+
if (restartBtn) {
|
|
225
|
+
restartBtn.textContent = "Restart";
|
|
226
|
+
restartBtn.disabled = false;
|
|
227
|
+
}
|
|
228
|
+
if (errorEl) {
|
|
229
|
+
errorEl.textContent = msg.error || "Restart failed";
|
|
230
|
+
errorEl.classList.remove("hidden");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function handleShutdownResult(msg) {
|
|
236
|
+
var shutdownInput = document.getElementById("settings-shutdown-input");
|
|
237
|
+
var shutdownBtn = document.getElementById("settings-shutdown-btn");
|
|
238
|
+
var errorEl = document.getElementById("settings-shutdown-error");
|
|
239
|
+
|
|
240
|
+
if (msg.ok) {
|
|
241
|
+
if (shutdownBtn) { shutdownBtn.textContent = "Server stopping..."; shutdownBtn.disabled = true; }
|
|
242
|
+
showToast("Server is shutting down...");
|
|
243
|
+
} else {
|
|
244
|
+
if (shutdownBtn) {
|
|
245
|
+
shutdownBtn.textContent = "Shut down…";
|
|
246
|
+
shutdownBtn.disabled = false;
|
|
247
|
+
}
|
|
248
|
+
if (shutdownInput) shutdownInput.disabled = false;
|
|
249
|
+
if (errorEl) {
|
|
250
|
+
errorEl.textContent = msg.error || "Shutdown failed";
|
|
251
|
+
errorEl.classList.remove("hidden");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// --- PIN form management ---
|
|
257
|
+
function showPinForm() {
|
|
258
|
+
var form = document.getElementById("settings-pin-form");
|
|
259
|
+
var input = document.getElementById("settings-pin-input");
|
|
260
|
+
var errorEl = document.getElementById("settings-pin-error");
|
|
261
|
+
if (form) form.classList.remove("hidden");
|
|
262
|
+
if (errorEl) errorEl.classList.add("hidden");
|
|
263
|
+
if (input) input.focus();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function hidePinForm() {
|
|
267
|
+
var form = document.getElementById("settings-pin-form");
|
|
268
|
+
var input = document.getElementById("settings-pin-input");
|
|
269
|
+
var errorEl = document.getElementById("settings-pin-error");
|
|
270
|
+
clearSettingsDraft("pin");
|
|
271
|
+
if (form) form.classList.add("hidden");
|
|
272
|
+
if (input) input.value = "";
|
|
273
|
+
if (errorEl) errorEl.classList.add("hidden");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function submitPin() {
|
|
277
|
+
var input = document.getElementById("settings-pin-input");
|
|
278
|
+
var errorEl = document.getElementById("settings-pin-error");
|
|
279
|
+
if (!input) return;
|
|
280
|
+
var pin = input.value.trim();
|
|
281
|
+
if (!/^\d{6}$/.test(pin)) {
|
|
282
|
+
if (errorEl) { errorEl.textContent = "Enter exactly six digits."; errorEl.classList.remove("hidden"); }
|
|
283
|
+
input.focus();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (errorEl) errorEl.classList.add("hidden");
|
|
287
|
+
var ws = getWs();
|
|
288
|
+
if (ws && ws.readyState === 1) {
|
|
289
|
+
sendSetting("set_pin", { pin: pin }, "settings-pin-save-btn", "settings-pin-error");
|
|
290
|
+
} else { status("settings-pin-error", "Disconnected. Reconnect and save again."); }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function updatePinStatus(enabled) {
|
|
294
|
+
var statusEl = document.getElementById("settings-pin-status");
|
|
295
|
+
var setBtn = document.getElementById("settings-pin-set-btn");
|
|
296
|
+
var removeBtn = document.getElementById("settings-pin-remove-btn");
|
|
297
|
+
var actionLabel = document.getElementById("settings-pin-action-label");
|
|
298
|
+
|
|
299
|
+
if (statusEl) {
|
|
300
|
+
statusEl.textContent = enabled ? "Enabled" : "Disabled";
|
|
301
|
+
statusEl.classList.toggle("settings-badge-green", enabled);
|
|
302
|
+
}
|
|
303
|
+
if (setBtn) setBtn.textContent = enabled ? "Change PIN" : "Set PIN";
|
|
304
|
+
if (removeBtn) removeBtn.classList.toggle("hidden", !enabled);
|
|
305
|
+
if (actionLabel) actionLabel.textContent = enabled ? "Change PIN" : "Set PIN";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
export function handleServerUpdate(msg) {
|
|
310
|
+
var button = document.getElementById('settings-install-update');
|
|
311
|
+
if (!button) return;
|
|
312
|
+
if (msg.type === 'update_available') { button.hidden = false; button.disabled = false; status('settings-update-status', 'Version ' + msg.version + ' is available. Review and restart when ready.'); }
|
|
313
|
+
if (msg.type === 'up_to_date') { button.hidden = true; status('settings-update-status', 'No newer release is available on this channel.'); }
|
|
314
|
+
if (msg.type === 'update_started') { button.disabled = true; status('settings-update-status', 'Updating. The connection will recover after the daemon restarts.'); }
|
|
315
|
+
if (msg.type === 'update_check_failed') { document.getElementById('settings-update-check').disabled = false; status('settings-update-status', msg.error || 'Update check failed. Retry when connected.'); }
|
|
316
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { refreshIcons } from './icons.js';
|
|
4
|
+
import { parseEnvString, looksLikeEnv, serializeEnvValue } from './settings-env-values.js';
|
|
5
|
+
import { setSettingsDraft } from './server-settings-state.js';
|
|
6
|
+
// ===== Shared Environment Variables =====
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
function showSharedEnvSaveStatus(text) {
|
|
11
|
+
var el = document.getElementById('ss-env-save-status');
|
|
12
|
+
if (el) el.textContent = text;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function loadSharedEnv() {
|
|
16
|
+
if (store.get("serverEnvLoaded") || store.get("serverEnvBusy")) return;
|
|
17
|
+
showSharedEnvSaveStatus("Loading…");
|
|
18
|
+
|
|
19
|
+
var ws = getWs();
|
|
20
|
+
if (ws && ws.readyState === 1) {
|
|
21
|
+
store.set({ serverEnvBusy: true });
|
|
22
|
+
ws.send(JSON.stringify({ type: "get_shared_env" }));
|
|
23
|
+
setTimeout(function () {
|
|
24
|
+
if (!store.get('serverEnvLoaded') && store.get('serverEnvBusy')) {
|
|
25
|
+
store.set({ serverEnvBusy: false });
|
|
26
|
+
showSharedEnvSaveStatus('No response received. Reopen Environment to retry.');
|
|
27
|
+
}
|
|
28
|
+
}, 12000);
|
|
29
|
+
} else showSharedEnvSaveStatus("Disconnected. Reopen this page after reconnecting.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function handleSharedEnv(msg) {
|
|
33
|
+
store.set({ serverEnvBusy: false });
|
|
34
|
+
if (store.get('serverEnvLoaded')) return;
|
|
35
|
+
if (msg.error) { showSharedEnvSaveStatus(msg.error); return; }
|
|
36
|
+
var list = document.getElementById("ss-env-list");
|
|
37
|
+
if (!list) return;
|
|
38
|
+
list.innerHTML = "";
|
|
39
|
+
|
|
40
|
+
var pairs = parseEnvString(msg.envrc || "");
|
|
41
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
42
|
+
addSharedEnvRow(pairs[i].key, pairs[i].value, false);
|
|
43
|
+
}
|
|
44
|
+
store.set({ serverEnvLoaded: true, serverEnvSaved: buildSharedEnvString() });
|
|
45
|
+
document.getElementById('ss-env-add-btn').disabled = false;
|
|
46
|
+
showSharedEnvSaveStatus('Changes apply to new coding-agent processes after saving.');
|
|
47
|
+
refreshIcons();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function handleSharedEnvSaved(msg) {
|
|
51
|
+
store.set({ serverEnvBusy: false });
|
|
52
|
+
document.getElementById('ss-env-save').disabled = false;
|
|
53
|
+
if (msg.ok) {
|
|
54
|
+
store.set({ serverEnvSaved: store.get('serverEnvSubmitted') });
|
|
55
|
+
markEnvironmentDraft();
|
|
56
|
+
showSharedEnvSaveStatus(buildSharedEnvString() !== store.get('serverEnvSaved') ? 'Submitted values saved. Newer edits remain unsaved.' : (msg.timing || 'Saved.'));
|
|
57
|
+
} else {
|
|
58
|
+
showSharedEnvSaveStatus("Error: " + (msg.error || "Failed to save"), 5000);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildSharedEnvString() {
|
|
63
|
+
var list = document.getElementById("ss-env-list");
|
|
64
|
+
if (!list) return "";
|
|
65
|
+
var rows = list.querySelectorAll(".ps-env-row");
|
|
66
|
+
var lines = [];
|
|
67
|
+
for (var i = 0; i < rows.length; i++) {
|
|
68
|
+
var keyInput = rows[i].querySelector(".ps-env-key");
|
|
69
|
+
var valInput = rows[i].querySelector(".ps-env-val");
|
|
70
|
+
var key = keyInput ? keyInput.value.trim() : "";
|
|
71
|
+
var val = valInput ? valInput.value : "";
|
|
72
|
+
if (key) lines.push("export " + key + "=" + serializeEnvValue(val));
|
|
73
|
+
}
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function addSharedEnvRow(key, value, focus) {
|
|
78
|
+
var list = document.getElementById("ss-env-list");
|
|
79
|
+
if (!list) return;
|
|
80
|
+
|
|
81
|
+
var row = document.createElement("div");
|
|
82
|
+
row.className = "ps-env-row";
|
|
83
|
+
|
|
84
|
+
var keyInput = document.createElement("input");
|
|
85
|
+
keyInput.type = "text";
|
|
86
|
+
keyInput.className = "ps-env-key";
|
|
87
|
+
keyInput.placeholder = "KEY";
|
|
88
|
+
keyInput.value = key;
|
|
89
|
+
keyInput.spellcheck = false;
|
|
90
|
+
keyInput.autocomplete = "off";
|
|
91
|
+
|
|
92
|
+
var valInput = document.createElement("input");
|
|
93
|
+
valInput.type = "password";
|
|
94
|
+
valInput.className = "ps-env-val";
|
|
95
|
+
valInput.placeholder = "value";
|
|
96
|
+
valInput.value = value;
|
|
97
|
+
valInput.spellcheck = false;
|
|
98
|
+
valInput.autocomplete = "off";
|
|
99
|
+
keyInput.setAttribute('aria-label', 'Variable name');
|
|
100
|
+
valInput.setAttribute('aria-label', 'Variable value');
|
|
101
|
+
var reveal = document.createElement('button'); reveal.type = 'button'; reveal.className = 'settings-btn-sm'; reveal.textContent = 'Show';
|
|
102
|
+
reveal.setAttribute('aria-label', 'Show variable value'); reveal.setAttribute('aria-pressed', 'false');
|
|
103
|
+
reveal.onclick = function () { var visible = valInput.type === 'password'; valInput.type = visible ? 'text' : 'password'; reveal.textContent = visible ? 'Hide' : 'Show'; reveal.setAttribute('aria-pressed', String(visible)); };
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
var delBtn = document.createElement("button");
|
|
107
|
+
delBtn.className = "ps-env-del";
|
|
108
|
+
delBtn.title = "Remove variable";
|
|
109
|
+
delBtn.setAttribute("aria-label", "Remove variable");
|
|
110
|
+
delBtn.innerHTML = '<i data-lucide="x"></i>';
|
|
111
|
+
|
|
112
|
+
delBtn.addEventListener("click", function () {
|
|
113
|
+
row.remove();
|
|
114
|
+
markEnvironmentDraft();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
keyInput.addEventListener("input", function () { markEnvironmentDraft(); });
|
|
118
|
+
valInput.addEventListener("input", function () { markEnvironmentDraft(); });
|
|
119
|
+
|
|
120
|
+
// Paste detection
|
|
121
|
+
keyInput.addEventListener("paste", function (e) {
|
|
122
|
+
var text = (e.clipboardData || window.clipboardData).getData("text");
|
|
123
|
+
if (text && looksLikeEnv(text)) {
|
|
124
|
+
e.preventDefault();
|
|
125
|
+
var pairs = parseEnvString(text);
|
|
126
|
+
if (pairs.length > 0) {
|
|
127
|
+
keyInput.value = pairs[0].key;
|
|
128
|
+
valInput.value = pairs[0].value;
|
|
129
|
+
for (var p = 1; p < pairs.length; p++) {
|
|
130
|
+
addSharedEnvRow(pairs[p].key, pairs[p].value, false);
|
|
131
|
+
}
|
|
132
|
+
markEnvironmentDraft();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
valInput.addEventListener("paste", function (e) {
|
|
138
|
+
var text = (e.clipboardData || window.clipboardData).getData("text");
|
|
139
|
+
if (text && text.indexOf("\n") !== -1 && text.indexOf("=") !== -1) {
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
var pairs = parseEnvString(text);
|
|
142
|
+
if (pairs.length > 0) {
|
|
143
|
+
keyInput.value = pairs[0].key;
|
|
144
|
+
valInput.value = pairs[0].value;
|
|
145
|
+
for (var p = 1; p < pairs.length; p++) {
|
|
146
|
+
addSharedEnvRow(pairs[p].key, pairs[p].value, false);
|
|
147
|
+
}
|
|
148
|
+
markEnvironmentDraft();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
row.appendChild(keyInput);
|
|
154
|
+
row.appendChild(valInput);
|
|
155
|
+
row.appendChild(reveal);
|
|
156
|
+
row.appendChild(delBtn);
|
|
157
|
+
list.appendChild(row);
|
|
158
|
+
refreshIcons();
|
|
159
|
+
|
|
160
|
+
if (focus) keyInput.focus();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function markEnvironmentDraft() {
|
|
164
|
+
var draft = buildSharedEnvString();
|
|
165
|
+
store.set({ serverEnvDraft: draft });
|
|
166
|
+
var dirty = draft !== store.get('serverEnvSaved') || Array.from(document.querySelectorAll('#ss-env-list .ps-env-key')).some(function (input) { return !input.value.trim(); });
|
|
167
|
+
setSettingsDraft('environment', dirty);
|
|
168
|
+
document.getElementById('ss-env-save').disabled = !dirty || store.get('serverEnvBusy');
|
|
169
|
+
showSharedEnvSaveStatus(dirty ? 'Unsaved changes' : 'No unsaved changes');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function initServerEnvironment() {
|
|
173
|
+
document.getElementById('ss-env-add-btn').disabled = true;
|
|
174
|
+
document.getElementById('ss-env-add-btn').onclick = function () { addSharedEnvRow('', '', true); markEnvironmentDraft(); };
|
|
175
|
+
document.getElementById('ss-env-save').onclick = function () {
|
|
176
|
+
var ws = getWs();
|
|
177
|
+
if (!ws || ws.readyState !== 1) { showSharedEnvSaveStatus('Disconnected. Your draft is retained.'); return; }
|
|
178
|
+
var invalid = Array.from(document.querySelectorAll('#ss-env-list .ps-env-key')).some(function (input) {
|
|
179
|
+
return !/^[A-Za-z_][A-Za-z0-9_]*$/.test(input.value.trim());
|
|
180
|
+
});
|
|
181
|
+
if (invalid) { showSharedEnvSaveStatus('Each variable needs a valid name using letters, digits and underscores, starting with a letter or underscore.'); return; }
|
|
182
|
+
var draft = buildSharedEnvString();
|
|
183
|
+
store.set({ serverEnvBusy: true, serverEnvSubmitted: draft });
|
|
184
|
+
this.disabled = true;
|
|
185
|
+
ws.send(JSON.stringify({ type: 'set_shared_env', envrc: draft }));
|
|
186
|
+
showSharedEnvSaveStatus('Saving…');
|
|
187
|
+
setTimeout(function () {
|
|
188
|
+
if (!store.get('serverEnvBusy')) return;
|
|
189
|
+
store.set({ serverEnvBusy: false });
|
|
190
|
+
document.getElementById('ss-env-save').disabled = false;
|
|
191
|
+
showSharedEnvSaveStatus('No acknowledgement received. Your draft is retained; reconnect and retry.');
|
|
192
|
+
}, 12000);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export var SERVER_SETTINGS_PAGES = [
|
|
2
|
+
{ id: 'overview', category: 'Service', label: 'Overview' },
|
|
3
|
+
{ id: 'network', category: 'Service', label: 'Network' },
|
|
4
|
+
{ id: 'storage', category: 'Service', label: 'Storage' },
|
|
5
|
+
{ id: 'security', category: 'Administration', label: 'Access & security' },
|
|
6
|
+
{ id: 'admin-users', category: 'Administration', label: 'Users', multiUser: true },
|
|
7
|
+
{ id: 'admin-projects', category: 'Administration', label: 'Project access', multiUser: true },
|
|
8
|
+
{ id: 'admin-smtp', category: 'Administration', label: 'Email delivery', multiUser: true },
|
|
9
|
+
{ id: 'environment', category: 'Agent configuration', label: 'Environment' },
|
|
10
|
+
{ id: 'operations', category: 'Maintenance', label: 'Operations' }
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function renderSettingsNavigation(multiUser, selectPage) {
|
|
14
|
+
var nav = document.getElementById('settings-page-nav');
|
|
15
|
+
var mobile = document.getElementById('settings-nav-dropdown');
|
|
16
|
+
nav.replaceChildren(); mobile.replaceChildren();
|
|
17
|
+
var category; var group;
|
|
18
|
+
SERVER_SETTINGS_PAGES.forEach(function (page) {
|
|
19
|
+
if (page.multiUser && !multiUser) return;
|
|
20
|
+
if (category !== page.category) {
|
|
21
|
+
category = page.category;
|
|
22
|
+
var heading = document.createElement('div'); heading.className = 'settings-nav-category'; heading.textContent = category; nav.appendChild(heading);
|
|
23
|
+
group = document.createElement('optgroup'); group.label = category; mobile.appendChild(group);
|
|
24
|
+
}
|
|
25
|
+
var button = document.createElement('button'); button.type = 'button'; button.className = 'settings-nav-item'; button.dataset.section = page.id; button.textContent = page.label;
|
|
26
|
+
button.onclick = function () { selectPage(page.id, true); }; nav.appendChild(button);
|
|
27
|
+
var option = document.createElement('option'); option.value = page.id; option.textContent = page.label; group.appendChild(option);
|
|
28
|
+
});
|
|
29
|
+
mobile.onchange = function () { selectPage(this.value, true); };
|
|
30
|
+
nav.onkeydown = function (event) {
|
|
31
|
+
if (['ArrowUp', 'ArrowDown', 'Home', 'End'].indexOf(event.key) < 0) return;
|
|
32
|
+
var items = Array.from(nav.querySelectorAll('button'));
|
|
33
|
+
var index = items.indexOf(document.activeElement);
|
|
34
|
+
if (index < 0) return;
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
index = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1 : (index + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
|
|
37
|
+
items[index].focus();
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
|
|
3
|
+
export function setSettingsDraft(key, dirty) {
|
|
4
|
+
var drafts = Object.assign({}, store.get('serverSettingsDrafts') || {});
|
|
5
|
+
drafts[key] = !!dirty;
|
|
6
|
+
store.set({ serverSettingsDrafts: drafts });
|
|
7
|
+
}
|
|
8
|
+
export function clearSettingsDraft(key) { setSettingsDraft(key, false); }
|
|
9
|
+
export function hasSettingsDrafts() {
|
|
10
|
+
var network = store.get('networkSettings');
|
|
11
|
+
var drafts = store.get('serverSettingsDrafts') || {};
|
|
12
|
+
return Object.keys(drafts).some(function (key) { return drafts[key]; }) ||
|
|
13
|
+
!!(network && network.loaded && network.draft !== network.saved);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function trapSettingsFocus(event, root) {
|
|
17
|
+
if (event.key !== 'Tab') return;
|
|
18
|
+
var items = Array.from(root.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), a[href], [tabindex="0"]')).filter(function (el) { return el.getClientRects().length; });
|
|
19
|
+
if (!items.length) return;
|
|
20
|
+
var first = items[0]; var last = items[items.length - 1];
|
|
21
|
+
if (event.shiftKey && (document.activeElement === first || !root.contains(document.activeElement))) { event.preventDefault(); last.focus(); }
|
|
22
|
+
else if (!event.shiftKey && (document.activeElement === last || !root.contains(document.activeElement))) { event.preventDefault(); first.focus(); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isolateSettingsDialog(overlay) {
|
|
26
|
+
var entries = Array.from(document.body.children).filter(function (el) { return el !== overlay && el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE'; }).map(function (el) {
|
|
27
|
+
var entry = { el: el, inert: el.inert, hidden: el.getAttribute('aria-hidden') };
|
|
28
|
+
el.inert = true; el.setAttribute('aria-hidden', 'true'); return entry;
|
|
29
|
+
});
|
|
30
|
+
return function () { entries.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); }); };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function confirmSettingsAction(message, action, onConfirm) {
|
|
34
|
+
if (document.getElementById('settings-confirm')) return;
|
|
35
|
+
var previous = document.activeElement;
|
|
36
|
+
var overlay = document.createElement('div');
|
|
37
|
+
overlay.id = 'settings-confirm'; overlay.className = 'settings-confirm';
|
|
38
|
+
overlay.innerHTML = '<div role="dialog" aria-modal="true" aria-labelledby="settings-confirm-title"><h3 id="settings-confirm-title"></h3><p></p><div class="settings-confirm-actions"><button class="settings-btn-sm" data-cancel>Keep editing</button><button class="settings-btn-sm" data-confirm></button></div></div>';
|
|
39
|
+
overlay.querySelector('h3').textContent = action;
|
|
40
|
+
overlay.querySelector('p').textContent = message;
|
|
41
|
+
overlay.querySelector('[data-confirm]').textContent = action;
|
|
42
|
+
function close() { restore(); overlay.remove(); if (previous && previous.isConnected) previous.focus(); }
|
|
43
|
+
overlay.querySelector('[data-cancel]').onclick = close;
|
|
44
|
+
overlay.querySelector('[data-confirm]').onclick = function () { close(); onConfirm(); };
|
|
45
|
+
overlay.addEventListener('keydown', function (event) {
|
|
46
|
+
event.stopPropagation();
|
|
47
|
+
if (event.key === 'Escape') { event.preventDefault(); close(); }
|
|
48
|
+
trapSettingsFocus(event, overlay);
|
|
49
|
+
});
|
|
50
|
+
overlay.addEventListener('click', function (event) { if (event.target === overlay) close(); });
|
|
51
|
+
document.body.appendChild(overlay);
|
|
52
|
+
var restore = isolateSettingsDialog(overlay);
|
|
53
|
+
overlay.querySelector('[data-cancel]').focus();
|
|
54
|
+
}
|