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,7 +1,7 @@
1
1
  import { store } from './store.js';
2
2
  import { mountDefaultAi, unmountDefaultAi } from './default-ai.js';
3
3
  import { openUserSettings } from './user-settings.js';
4
- import { getProfile, updateProfileName, regenerateProfileAvatar, showAvatarPositioner, uploadAvatarBlob } from './profile.js';
4
+ import { getProfile, saveProfileUpdates, regenerateProfileAvatar, showAvatarPositioner, uploadAvatarBlob } from './profile.js';
5
5
  import { requestCursorSharingState, setCursorSharing } from './app-cursors.js';
6
6
  import { registerTooltip } from './tooltip.js';
7
7
 
@@ -69,7 +69,8 @@ function startNameEdit() {
69
69
  input.type = 'text';
70
70
  input.maxLength = 50;
71
71
  input.autocomplete = 'name';
72
- input.value = getProfile().name || '';
72
+ input.value = store.get('accountNameDraft') === undefined ? getProfile().name || '' : store.get('accountNameDraft');
73
+ input.addEventListener('input',function(){store.set({accountNameDraft:input.value});});
73
74
  section.innerHTML = '';
74
75
  section.appendChild(input);
75
76
  var hint = document.createElement('small');
@@ -78,11 +79,11 @@ function startNameEdit() {
78
79
  input.addEventListener('keydown', function(event) {
79
80
  if (event.key === 'Enter') {
80
81
  event.preventDefault();
81
- updateProfileName(input.value);
82
- renderNameResting(true);
82
+ input.disabled=true;hint.textContent='Saving…';
83
+ saveProfileUpdates({name:input.value.trim()}).then(function(){store.set({accountNameDraft:undefined});renderNameResting(true);}).catch(function(error){input.disabled=false;hint.textContent=error.message;input.focus();});
83
84
  } else if (event.key === 'Escape') {
84
85
  event.preventDefault();
85
- renderNameResting(true);
86
+ store.set({accountNameDraft:undefined});renderNameResting(true);
86
87
  }
87
88
  event.stopPropagation();
88
89
  });
@@ -180,7 +181,7 @@ function updateCursorAction() {
180
181
  registerTooltip(button, tooltipText);
181
182
  }
182
183
 
183
- function onSettings(event) { event.stopPropagation(); close(false); openUserSettings(); }
184
+ function onSettings(event) { event.stopPropagation(); close(true); openUserSettings(); }
184
185
  function onCursorAction(event) {
185
186
  event.stopPropagation();
186
187
  if (event.currentTarget.disabled) return;
@@ -215,7 +216,8 @@ export function initAccountIsland() {
215
216
  updateCursorAction();
216
217
  trigger.setAttribute('aria-label', 'Open account menu for ' + accountName());
217
218
  store.subscribe(function(state, previous) {
218
- if (state.myUserId !== previous.myUserId || state.connected === false) close(false);
219
+ if(state.myUserId !== previous.myUserId && state.accountNameDraft!==undefined)store.set({accountNameDraft:undefined});
220
+ if (state.myUserId !== previous.myUserId || state.connected === false && previous.connected !== false) close(false);
219
221
  updateCursorAction();
220
222
  if (popover) position();
221
223
  });
@@ -0,0 +1,197 @@
1
+ import { store } from './store.js';
2
+ import { iconHtml, refreshIcons } from './icons.js';
3
+ import { showToast, copyToClipboard, escapeHtml } from './utils.js';
4
+ import { apiGet, apiPost, apiPut, apiDelete, showConfirmDialog, showConfirmResetPin, prepareAdminModal } from './admin-settings-ui.js';
5
+ import { setSettingsDraft, clearSettingsDraft } from './server-settings-state.js';
6
+ // --- SMTP Configuration ---
7
+ export function loadSmtpTab(body) {
8
+ apiGet("/api/admin/smtp").then(function (data) {
9
+ if (!(store.get('serverSettingsDrafts') || {}).smtp) renderSmtpOverview(body, data.smtp);
10
+ }).catch(function () {
11
+ body.innerHTML = '<div class="admin-error">Failed to load SMTP settings</div>';
12
+ });
13
+ }
14
+
15
+ function renderSmtpOverview(body, cfg, focus) {
16
+ var configured = !!(cfg && cfg.host);
17
+ clearSettingsDraft('smtp');
18
+ body.oninput = null;
19
+ body.onchange = null;
20
+ var html = '<div class="settings-section-heading"><h3>Configuration</h3><button type="button" class="settings-btn-sm" id="smtp-edit">Edit</button></div>';
21
+ html += '<p class="settings-hint">' + (configured ? 'SMTP configured. New email deliveries use the saved configuration.' : 'SMTP is not configured. Users sign in with a PIN. Choose Edit to set up email delivery.') + '</p>';
22
+ if (configured) {
23
+ var fields = [
24
+ ['SMTP host', cfg.host], ['Port', String(cfg.port || 587)],
25
+ ['Connection', cfg.secure ? 'Direct TLS/SSL' : 'STARTTLS'],
26
+ ['Username', cfg.user || '—'], ['Password', cfg.pass ? 'Configured · hidden' : 'Not configured'],
27
+ ['From address', cfg.from || '—'], ['Email login', cfg.emailLoginEnabled ? 'Enabled' : 'Disabled']
28
+ ];
29
+ html += '<dl class="settings-summary">';
30
+ fields.forEach(function (field) {
31
+ html += '<div class="settings-field-row"><dt class="settings-label">' + escapeHtml(field[0]) + '</dt><dd class="settings-value">' + escapeHtml(field[1]) + '</dd></div>';
32
+ });
33
+ html += '</dl><p class="settings-hint">Configuration is saved; delivery is not verified here. Edit to change settings or send a test email.</p>';
34
+ }
35
+ body.innerHTML = html;
36
+ var edit = body.querySelector('#smtp-edit');
37
+ edit.onclick = function () { renderSmtpTab(body, cfg); body.querySelector('#smtp-host').focus(); };
38
+ if (focus) edit.focus();
39
+ }
40
+
41
+ function renderSmtpTab(body, cfg) {
42
+ var hasConfig = !!(cfg && cfg.host);
43
+ var html = '<div class="admin-smtp-form">';
44
+
45
+ var emailEnabled = !!(cfg && cfg.emailLoginEnabled);
46
+ if (hasConfig) {
47
+ html += '<div class="admin-smtp-status admin-smtp-status-ok">' + iconHtml("check-circle") + ' SMTP configured. New email deliveries use the saved configuration.</div>';
48
+ } else {
49
+ html += '<div class="admin-smtp-status admin-smtp-status-off">' + iconHtml("mail-x") + ' SMTP not configured. Users log in with a PIN instead of email codes.</div>';
50
+ }
51
+
52
+ html += '<div class="admin-smtp-fields">';
53
+ html += '<div class="admin-smtp-row">' +
54
+ '<label for="smtp-host">SMTP host</label>' +
55
+ '<input type="text" id="smtp-host" class="admin-smtp-input" placeholder="smtp.gmail.com" value="' + escapeHtml((cfg && cfg.host) || "") + '">' +
56
+ '</div>';
57
+ html += '<div class="admin-smtp-row-half">' +
58
+ '<div class="admin-smtp-row">' +
59
+ '<label for="smtp-port">Port</label>' +
60
+ '<input type="number" id="smtp-port" class="admin-smtp-input" placeholder="587" value="' + ((cfg && cfg.port) || 587) + '">' +
61
+ '</div>' +
62
+ '<div class="admin-smtp-row">' +
63
+ '<label for="smtp-secure">Secure (TLS)</label>' +
64
+ '<label class="admin-smtp-toggle"><input type="checkbox" id="smtp-secure"' + (cfg && cfg.secure ? " checked" : "") + '><span>Use TLS/SSL</span></label>' +
65
+ '</div></div>';
66
+ html += '<div class="admin-smtp-row">' +
67
+ '<label for="smtp-user">Username</label>' +
68
+ '<input type="text" id="smtp-user" class="admin-smtp-input" placeholder="you@gmail.com" value="' + escapeHtml((cfg && cfg.user) || "") + '" autocomplete="off">' +
69
+ '</div>';
70
+ html += '<div class="admin-smtp-row">' +
71
+ '<label for="smtp-pass">Password</label>' +
72
+ '<input type="password" id="smtp-pass" class="admin-smtp-input" placeholder="App password" value="' + escapeHtml((cfg && cfg.pass) || "") + '" autocomplete="off">' +
73
+ '</div>';
74
+ html += '<div class="admin-smtp-row">' +
75
+ '<label for="smtp-from">From address</label>' +
76
+ '<input type="text" id="smtp-from" class="admin-smtp-input" placeholder="Clay <noreply@example.com>" value="' + escapeHtml((cfg && cfg.from) || "") + '">' +
77
+ '</div>';
78
+ html += '<div class="admin-smtp-row admin-smtp-row-otp">' +
79
+ '<label for="smtp-email-login">Email login (OTP)</label>' +
80
+ '<label class="admin-smtp-toggle"><input type="checkbox" id="smtp-email-login"' + (emailEnabled ? " checked" : "") + (hasConfig ? "" : " disabled") + '>' +
81
+ '<span>' + (hasConfig ? "Require email for user registration and enable OTP login" : "Configure SMTP first to enable") + '</span></label>' +
82
+ '</div>';
83
+ html += '</div>';
84
+
85
+ html += '<div class="admin-smtp-actions">';
86
+ html += '<button type="button" class="admin-action-btn admin-action-btn-secondary" id="smtp-cancel">Cancel</button>';
87
+ html += '<button class="admin-action-btn" id="smtp-save">' + iconHtml("save") + ' Save</button>';
88
+ html += '<button class="admin-action-btn admin-action-btn-secondary" id="smtp-test">' + iconHtml("send") + ' Send test email</button>';
89
+ if (hasConfig) {
90
+ html += '<button class="admin-action-btn admin-action-btn-danger" id="smtp-remove">' + iconHtml("trash-2") + ' Remove</button>';
91
+ }
92
+ html += '</div>';
93
+ html += '<div class="admin-smtp-error" id="smtp-error"></div>';
94
+ html += '<div class="admin-smtp-hint">For Gmail, use an <a href="https://support.google.com/accounts/answer/185833" target="_blank" rel="noopener">App Password</a>. Port 587 with TLS off uses STARTTLS. Port 465 with TLS on uses direct SSL.</div>';
95
+ html += '</div>';
96
+
97
+ body.innerHTML = html;
98
+ clearSettingsDraft('smtp');
99
+ body.oninput = function () { setSettingsDraft('smtp', true); };
100
+ body.onchange = function () { setSettingsDraft('smtp', true); };
101
+ refreshIcons(body);
102
+
103
+ var errorEl = body.querySelector("#smtp-error");
104
+ body.querySelector('#smtp-cancel').onclick = function () {
105
+ if (body.querySelector('#smtp-save').disabled) return;
106
+ function cancel() { renderSmtpOverview(body, cfg, true); }
107
+ if ((store.get('serverSettingsDrafts') || {}).smtp) showConfirmDialog('Discard your unsaved email delivery changes?', 'Discard changes', cancel);
108
+ else cancel();
109
+ };
110
+
111
+ function getFormData() {
112
+ return {
113
+ host: body.querySelector("#smtp-host").value.trim(),
114
+ port: parseInt(body.querySelector("#smtp-port").value, 10) || 587,
115
+ secure: body.querySelector("#smtp-secure").checked,
116
+ user: body.querySelector("#smtp-user").value.trim(),
117
+ pass: body.querySelector("#smtp-pass").value,
118
+ from: body.querySelector("#smtp-from").value.trim(),
119
+ emailLoginEnabled: body.querySelector("#smtp-email-login").checked,
120
+ };
121
+ }
122
+
123
+ // Save
124
+ body.querySelector("#smtp-save").addEventListener("click", function () {
125
+ var formData = getFormData();
126
+ if (!formData.host || !formData.user || !formData.pass || !formData.from) {
127
+ errorEl.textContent = "All fields are required";
128
+ errorEl.className = "admin-smtp-error admin-smtp-error-visible";
129
+ return;
130
+ }
131
+ var btn = this;
132
+ btn.disabled = true;
133
+ body.querySelectorAll("input").forEach(function (input) { input.disabled = true; });
134
+ errorEl.textContent = "";
135
+ errorEl.className = "admin-smtp-error";
136
+ apiPost("/api/admin/smtp", formData).then(function (data) {
137
+ if (data.ok) {
138
+ clearSettingsDraft("smtp");
139
+ showToast("SMTP settings saved. New email deliveries use this configuration.");
140
+ renderSmtpOverview(body, Object.assign({}, formData, { pass: '••••••••' }), true);
141
+ } else {
142
+ errorEl.textContent = data.error || "Failed to save";
143
+ errorEl.className = "admin-smtp-error admin-smtp-error-visible";
144
+ btn.disabled = false;
145
+ body.querySelectorAll("input").forEach(function (input) { input.disabled = input.id === "smtp-email-login" && !hasConfig; });
146
+ }
147
+ }).catch(function () {
148
+ errorEl.textContent = "Failed to save settings";
149
+ errorEl.className = "admin-smtp-error admin-smtp-error-visible";
150
+ btn.disabled = false;
151
+ body.querySelectorAll("input").forEach(function (input) { input.disabled = input.id === "smtp-email-login" && !hasConfig; });
152
+ });
153
+ });
154
+
155
+ // Test
156
+ body.querySelector("#smtp-test").addEventListener("click", function () {
157
+ var formData = getFormData();
158
+ if (!formData.host || !formData.user || !formData.pass || !formData.from) {
159
+ errorEl.textContent = "Fill in all fields first";
160
+ errorEl.className = "admin-smtp-error admin-smtp-error-visible";
161
+ return;
162
+ }
163
+ var btn = this;
164
+ btn.disabled = true;
165
+ errorEl.textContent = "";
166
+ errorEl.className = "admin-smtp-error";
167
+ apiPost("/api/admin/smtp/test", formData).then(function (data) {
168
+ if (data.ok) {
169
+ showToast(data.message || "Test email sent!");
170
+ errorEl.textContent = "";
171
+ errorEl.className = "admin-smtp-error";
172
+ } else {
173
+ errorEl.textContent = data.error || "Test failed";
174
+ errorEl.className = "admin-smtp-error admin-smtp-error-visible";
175
+ }
176
+ btn.disabled = false;
177
+ }).catch(function () {
178
+ errorEl.textContent = "Connection failed";
179
+ errorEl.className = "admin-smtp-error admin-smtp-error-visible";
180
+ btn.disabled = false;
181
+ });
182
+ });
183
+
184
+ // Remove
185
+ var removeBtn = body.querySelector("#smtp-remove");
186
+ if (removeBtn) {
187
+ removeBtn.addEventListener("click", function () {
188
+ showConfirmDialog("Remove SMTP configuration? Users will need to use PIN login.", "Remove SMTP", function () {
189
+ apiPost("/api/admin/smtp", { host: "", user: "", pass: "", from: "" }).then(function (data) {
190
+ if (!data.ok) throw new Error(data.error || "Could not remove SMTP configuration");
191
+ showToast("SMTP configuration removed");
192
+ renderSmtpOverview(body, null, true);
193
+ }).catch(function (error) { errorEl.textContent = error.message; errorEl.className = "admin-smtp-error admin-smtp-error-visible"; });
194
+ });
195
+ });
196
+ }
197
+ }
@@ -0,0 +1,183 @@
1
+ import { store } from './store.js';
2
+ import { iconHtml, refreshIcons } from './icons.js';
3
+ import { showToast, copyToClipboard, escapeHtml } from './utils.js';
4
+ import { apiGet, apiPost, apiPut, apiDelete, showConfirmDialog, showConfirmResetPin, prepareAdminModal } from './admin-settings-ui.js';
5
+ // --- Invites ---
6
+ export function loadInvitesTab(body) {
7
+ apiGet("/api/admin/invites").then(function (data) {
8
+ store.set({ adminInvites: (data.invites || []).filter(function (inv) { return !inv.used && inv.expiresAt > Date.now(); }) });
9
+ renderInvitesTab(body);
10
+ }).catch(function () {
11
+ body.innerHTML = '<div class="admin-error">Failed to load invites</div>';
12
+ });
13
+ }
14
+
15
+ function renderInvitesTab(body) {
16
+ var smtpEnabled = store.get('adminMe') && store.get('adminMe').smtpEnabled;
17
+ var html = '<div class="admin-section-header">' +
18
+ '<div class="admin-header-btns">';
19
+ if (smtpEnabled) {
20
+ html += '<button class="admin-action-btn" id="admin-email-invite">' + iconHtml("mail") + ' Email Invite</button>';
21
+ }
22
+ html += '<button class="admin-action-btn admin-action-btn-secondary" id="admin-create-invite">' + iconHtml("plus") + ' Generate Link</button>' +
23
+ '</div></div>';
24
+
25
+ if (store.get('adminInvites').length === 0) {
26
+ html += '<div class="admin-empty">No active invites. Generate one to add a new user.</div>';
27
+ } else {
28
+ html += '<div class="admin-invite-list">';
29
+ for (var i = 0; i < store.get('adminInvites').length; i++) {
30
+ var inv = store.get('adminInvites')[i];
31
+ var expiresIn = Math.max(0, Math.ceil((inv.expiresAt - Date.now()) / (60 * 60 * 1000)));
32
+ html += '<div class="admin-invite-item">';
33
+ html += '<div class="admin-invite-info">';
34
+ html += '<code class="admin-invite-code">' + escapeHtml(inv.code.substring(0, 8)) + '...</code>';
35
+ if (inv.email) html += '<span class="admin-invite-email">' + escapeHtml(inv.email) + '</span>';
36
+ html += '<span class="admin-invite-expiry">expires in ' + expiresIn + 'h</span>';
37
+ html += '</div>';
38
+ html += '<div class="admin-invite-actions">';
39
+ html += '<button class="admin-copy-link-btn" data-code="' + escapeHtml(inv.code) + '" title="Copy link">' + iconHtml("copy") + '</button>';
40
+ html += '<button class="admin-revoke-btn" data-code="' + escapeHtml(inv.code) + '" title="Revoke">' + iconHtml("x") + '</button>';
41
+ html += '</div>';
42
+ html += '</div>';
43
+ }
44
+ html += '</div>';
45
+ }
46
+
47
+ body.innerHTML = html;
48
+ refreshIcons(body);
49
+
50
+ // Generate invite link
51
+ var createBtn = body.querySelector("#admin-create-invite");
52
+ if (createBtn) {
53
+ createBtn.addEventListener("click", function () {
54
+ createInvite(body);
55
+ });
56
+ }
57
+
58
+ // Email invite
59
+ var emailBtn = body.querySelector("#admin-email-invite");
60
+ if (emailBtn) {
61
+ emailBtn.addEventListener("click", function () {
62
+ showEmailInvitePrompt(body);
63
+ });
64
+ }
65
+
66
+ // Copy link buttons
67
+ var copyBtns = body.querySelectorAll(".admin-copy-link-btn");
68
+ for (var j = 0; j < copyBtns.length; j++) {
69
+ copyBtns[j].addEventListener("click", function () {
70
+ var code = this.dataset.code;
71
+ var url = location.origin + "/invite/" + code;
72
+ copyToClipboard(url).then(function () {
73
+ showToast("Invite link copied");
74
+ });
75
+ });
76
+ }
77
+
78
+ // Revoke buttons
79
+ var revokeBtns = body.querySelectorAll(".admin-revoke-btn");
80
+ for (var k = 0; k < revokeBtns.length; k++) {
81
+ revokeBtns[k].addEventListener("click", function () {
82
+ var code = this.dataset.code;
83
+ var btn = this;
84
+ showConfirmDialog("Revoke this invite? The link will no longer work.", "Revoke", function () {
85
+ btn.disabled = true;
86
+ fetch("/api/admin/invites/" + encodeURIComponent(code), { method: "DELETE" })
87
+ .then(function (r) { return r.json(); })
88
+ .then(function (d) {
89
+ if (d.ok) {
90
+ showToast("Invite revoked");
91
+ loadInvitesTab(body);
92
+ } else {
93
+ showToast(d.error || "Failed to revoke");
94
+ btn.disabled = false;
95
+ }
96
+ })
97
+ .catch(function () {
98
+ showToast("Failed to revoke invite");
99
+ btn.disabled = false;
100
+ });
101
+ });
102
+ });
103
+ }
104
+ }
105
+
106
+ function createInvite(body) {
107
+ apiPost("/api/admin/invites").then(function (data) {
108
+ if (data.ok && data.url) {
109
+ copyToClipboard(data.url).then(function () {
110
+ showToast("Invite link created and copied!");
111
+ }).catch(function () {
112
+ showToast("Invite created: " + data.url);
113
+ });
114
+ loadInvitesTab(body);
115
+ } else {
116
+ showToast(data.error || "Failed to create invite");
117
+ }
118
+ }).catch(function () {
119
+ showToast("Failed to create invite");
120
+ });
121
+ }
122
+
123
+ function showEmailInvitePrompt(body) {
124
+ var modal = document.createElement("div");
125
+ modal.className = "admin-modal-overlay";
126
+ var html = '<div class="admin-modal">' +
127
+ '<div class="admin-modal-header">' +
128
+ '<h3>Send Email Invite</h3>' +
129
+ '<button class="admin-modal-close">' + iconHtml("x") + '</button>' +
130
+ '</div>' +
131
+ '<div class="admin-modal-body">' +
132
+ '<p class="admin-modal-desc">Enter the email address to send an invitation to:</p>' +
133
+ '<input type="email" class="admin-smtp-input" id="admin-invite-email" placeholder="user@example.com" autocomplete="off">' +
134
+ '<div class="admin-smtp-error" id="admin-invite-error"></div>' +
135
+ '</div>' +
136
+ '<div class="admin-modal-footer">' +
137
+ '<button class="admin-modal-save" id="admin-invite-send">Send Invite</button>' +
138
+ '<button class="admin-modal-cancel">Cancel</button>' +
139
+ '</div></div>';
140
+ modal.innerHTML = html;
141
+ document.body.appendChild(modal);
142
+ prepareAdminModal(modal);
143
+ refreshIcons(modal);
144
+
145
+ var emailInput = modal.querySelector("#admin-invite-email");
146
+ var sendBtn = modal.querySelector("#admin-invite-send");
147
+ var errorEl = modal.querySelector("#admin-invite-error");
148
+
149
+ modal.querySelector(".admin-modal-close").addEventListener("click", function () { modal.remove(); });
150
+ modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
151
+ modal.addEventListener("click", function (e) { if (e.target === modal) modal.remove(); });
152
+
153
+ emailInput.focus();
154
+ emailInput.addEventListener("keydown", function (e) {
155
+ if (e.key === "Enter") sendBtn.click();
156
+ });
157
+
158
+ sendBtn.addEventListener("click", function () {
159
+ var email = emailInput.value.trim();
160
+ if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
161
+ errorEl.textContent = "Enter a valid email address";
162
+ return;
163
+ }
164
+ sendBtn.disabled = true;
165
+ sendBtn.textContent = "Sending...";
166
+ errorEl.textContent = "";
167
+ apiPost("/api/admin/invites/email", { email: email }).then(function (data) {
168
+ if (data.ok) {
169
+ showToast("Invite sent to " + email);
170
+ modal.remove();
171
+ loadInvitesTab(body);
172
+ } else {
173
+ errorEl.textContent = data.error || "Failed to send invite";
174
+ sendBtn.disabled = false;
175
+ sendBtn.textContent = "Send Invite";
176
+ }
177
+ }).catch(function () {
178
+ errorEl.textContent = "Failed to send invite";
179
+ sendBtn.disabled = false;
180
+ sendBtn.textContent = "Send Invite";
181
+ });
182
+ });
183
+ }
@@ -0,0 +1,198 @@
1
+ import { store } from './store.js';
2
+ import { iconHtml, refreshIcons } from './icons.js';
3
+ import { showToast, copyToClipboard, escapeHtml } from './utils.js';
4
+ import { apiGet, apiPost, apiPut, apiDelete, showConfirmDialog, showConfirmResetPin, prepareAdminModal } from './admin-settings-ui.js';
5
+ // --- Projects ---
6
+ export function loadProjectsTab(body) {
7
+ var projectList = store.get('projectsHubList') || [];
8
+ // Exclude worktree and mate projects (mates are always private to their owner)
9
+ projectList = projectList.filter(function (p) { return !p.isWorktree && !p.isMate; });
10
+
11
+
12
+ if (projectList.length === 0) {
13
+ body.innerHTML = '<div class="admin-empty">No projects registered.</div>';
14
+ return;
15
+ }
16
+
17
+ var accessPromises = projectList.map(function (p) {
18
+ return apiGet("/api/admin/projects/" + p.slug + "/access").then(function (access) {
19
+ return { slug: p.slug, title: p.title || p.project || p.slug, projectOwnerId: p.projectOwnerId || null, visibility: access.visibility || "public", allowedUsers: access.allowedUsers || [] };
20
+ });
21
+ });
22
+
23
+ Promise.all([
24
+ Promise.all(accessPromises),
25
+ apiGet("/api/admin/users"),
26
+ ]).then(function (results) {
27
+ renderProjectsTab(body, results[0], results[1].users || []);
28
+ }).catch(function (error) { body.textContent = error.message || "Project access unavailable. Reopen Settings to retry."; });
29
+ }
30
+
31
+ function renderProjectsTab(body, projectAccessList, allUsers) {
32
+ var html = '<div class="admin-project-list">';
33
+ for (var i = 0; i < projectAccessList.length; i++) {
34
+ var p = projectAccessList[i];
35
+ var visClass = p.visibility === "private" ? "admin-vis-private" : "admin-vis-public";
36
+ html += '<div class="admin-project-item" data-slug="' + escapeHtml(p.slug) + '">';
37
+ html += '<div class="admin-project-info">';
38
+ html += '<div class="admin-project-name">' + escapeHtml(p.title) + '</div>';
39
+ html += '<div class="admin-project-slug">' + escapeHtml(p.slug) + '</div>';
40
+ html += '</div>';
41
+ html += '<div class="admin-project-controls">';
42
+ // Owner select
43
+ html += '<select aria-label="Project owner" class="admin-owner-select" data-slug="' + escapeHtml(p.slug) + '">';
44
+ html += '<option value=""' + (!p.projectOwnerId ? ' selected' : '') + '>No owner</option>';
45
+ for (var u = 0; u < allUsers.length; u++) {
46
+ var user = allUsers[u];
47
+ var sel = p.projectOwnerId === user.id ? ' selected' : '';
48
+ var label = escapeHtml(user.displayName || user.username);
49
+ if (user.linuxUser) label += ' (' + escapeHtml(user.linuxUser) + ')';
50
+ html += '<option value="' + escapeHtml(user.id) + '"' + sel + '>' + label + '</option>';
51
+ }
52
+ html += '</select>';
53
+ // Visibility select
54
+ html += '<select aria-label="Project visibility" class="admin-vis-select ' + visClass + '" data-slug="' + escapeHtml(p.slug) + '">';
55
+ html += '<option value="public"' + (p.visibility === "public" ? " selected" : "") + '>Public</option>';
56
+ html += '<option value="private"' + (p.visibility === "private" ? " selected" : "") + '>Private</option>';
57
+ html += '</select>';
58
+ if (p.visibility === "private") {
59
+ html += '<button aria-label="Manage allowed users" class="admin-manage-users-btn" data-slug="' + escapeHtml(p.slug) + '">' + iconHtml("users") + '</button>';
60
+ }
61
+ html += '</div>';
62
+ html += '</div>';
63
+ }
64
+ html += '</div>';
65
+
66
+ body.innerHTML = html;
67
+ refreshIcons(body);
68
+
69
+ // Bind owner selects
70
+ var ownerSelects = body.querySelectorAll(".admin-owner-select");
71
+ for (var oi = 0; oi < ownerSelects.length; oi++) {
72
+ ownerSelects[oi].dataset.savedValue = ownerSelects[oi].value;
73
+ ownerSelects[oi].addEventListener("change", function () {
74
+ var slug = this.dataset.slug;
75
+ var userId = this.value;
76
+ this.value = this.dataset.savedValue; this.dispatchEvent(new Event("settings-sync"));
77
+ showConfirmDialog("Transfer project ownership? New processes will use the selected owner’s identity where OS-user mode is enabled.", "Transfer ownership", function () { setProjectOwner(slug, userId, body); });
78
+ });
79
+ }
80
+
81
+ // Bind visibility selects
82
+ var visSelects = body.querySelectorAll(".admin-vis-select");
83
+ for (var j = 0; j < visSelects.length; j++) {
84
+ visSelects[j].dataset.savedValue = visSelects[j].value;
85
+ visSelects[j].addEventListener("change", function () {
86
+ var slug = this.dataset.slug;
87
+ var visibility = this.value;
88
+ this.value = this.dataset.savedValue; this.dispatchEvent(new Event("settings-sync"));
89
+ showConfirmDialog("Change project visibility to " + visibility + "? This changes who can access this project.", "Change visibility", function () { setProjectVisibility(slug, visibility, body); });
90
+ });
91
+ }
92
+
93
+ // Bind manage users buttons
94
+ var manageUserBtns = body.querySelectorAll(".admin-manage-users-btn");
95
+ for (var k = 0; k < manageUserBtns.length; k++) {
96
+ manageUserBtns[k].addEventListener("click", function () {
97
+ var slug = this.dataset.slug;
98
+ showProjectUsersModal(slug, body);
99
+ });
100
+ }
101
+ }
102
+
103
+ function setProjectOwner(slug, userId, body) {
104
+ apiPut("/api/admin/projects/" + slug + "/owner", { userId: userId || null }).then(function (data) {
105
+ if (data.ok) {
106
+ showToast("Project owner updated");
107
+ loadProjectsTab(body);
108
+ } else {
109
+ showToast(data.error || "Failed to update owner", "error");
110
+ }
111
+ }).catch(function () {
112
+ showToast("Failed to update owner", "error");
113
+ });
114
+ }
115
+
116
+ function setProjectVisibility(slug, visibility, body) {
117
+ apiPut("/api/admin/projects/" + slug + "/visibility", { visibility: visibility }).then(function (data) {
118
+ if (data.ok) {
119
+ showToast("Visibility updated");
120
+ loadProjectsTab(body);
121
+ } else {
122
+ showToast(data.error || "Failed to update visibility");
123
+ }
124
+ }).catch(function () {
125
+ showToast("Failed to update visibility");
126
+ });
127
+ }
128
+
129
+ function showProjectUsersModal(slug, parentBody) {
130
+ Promise.all([
131
+ apiGet("/api/admin/users"),
132
+ apiGet("/api/admin/projects/" + slug + "/access"),
133
+ ]).then(function (results) {
134
+ var allUsers = results[0].users || [];
135
+ var access = results[1];
136
+ var allowed = access.allowedUsers || [];
137
+
138
+ var modal = document.createElement("div");
139
+ modal.className = "admin-modal-overlay";
140
+
141
+ var html = '<div class="admin-modal">';
142
+ html += '<div class="admin-modal-header">';
143
+ html += '<h3>Manage Access: ' + escapeHtml(slug) + '</h3>';
144
+ html += '<button class="admin-modal-close">' + iconHtml("x") + '</button>';
145
+ html += '</div>';
146
+ html += '<div class="admin-modal-body">';
147
+ html += '<p class="admin-modal-desc">Select users who can access this private project:</p>';
148
+
149
+ for (var i = 0; i < allUsers.length; i++) {
150
+ var u = allUsers[i];
151
+ if (u.role === "admin") continue;
152
+ var checked = allowed.indexOf(u.id) >= 0 ? " checked" : "";
153
+ html += '<label class="admin-user-check">';
154
+ html += '<input type="checkbox" value="' + u.id + '"' + checked + '>';
155
+ html += '<span>' + escapeHtml(u.displayName || u.username) + ' <small>' + escapeHtml(u.username) + '</small></span>';
156
+ html += '</label>';
157
+ }
158
+
159
+ html += '</div>';
160
+ html += '<div class="admin-modal-footer">';
161
+ html += '<button class="admin-modal-save">Save</button>';
162
+ html += '<button class="admin-modal-cancel">Cancel</button>';
163
+ html += '</div>';
164
+ html += '</div>';
165
+
166
+ modal.innerHTML = html;
167
+ document.body.appendChild(modal);
168
+ prepareAdminModal(modal);
169
+ refreshIcons(modal);
170
+
171
+ modal.querySelector(".admin-modal-close").addEventListener("click", function () { modal.remove(); });
172
+ modal.querySelector(".admin-modal-cancel").addEventListener("click", function () { modal.remove(); });
173
+ modal.addEventListener("click", function (e) {
174
+ if (e.target === modal) modal.remove();
175
+ });
176
+
177
+ modal.querySelector(".admin-modal-save").addEventListener("click", function () {
178
+ var checkboxes = modal.querySelectorAll('input[type="checkbox"]');
179
+ var selectedUsers = [];
180
+ for (var ci = 0; ci < checkboxes.length; ci++) {
181
+ if (checkboxes[ci].checked) selectedUsers.push(checkboxes[ci].value);
182
+ }
183
+ apiPut("/api/admin/projects/" + slug + "/users", { allowedUsers: selectedUsers }).then(function (data) {
184
+ if (data.ok) {
185
+ showToast("Project access updated");
186
+ modal.remove();
187
+ loadProjectsTab(parentBody);
188
+ } else {
189
+ showToast(data.error || "Failed to update access");
190
+ }
191
+ }).catch(function () {
192
+ showToast("Failed to update access");
193
+ });
194
+ });
195
+ }).catch(function () {
196
+ showToast("Failed to load project access info");
197
+ });
198
+ }