clay-server 2.11.0-beta.9 → 2.11.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.
- package/bin/cli.js +282 -115
- package/lib/daemon.js +109 -37
- package/lib/os-users.js +58 -1
- package/lib/pages.js +31 -29
- package/lib/project.js +27 -8
- package/lib/public/app.js +158 -16
- package/lib/public/css/filebrowser.css +6 -0
- package/lib/public/css/icon-strip.css +123 -1
- package/lib/public/css/messages.css +1 -1
- package/lib/public/css/mobile-nav.css +17 -0
- package/lib/public/css/overlays.css +49 -0
- package/lib/public/css/sidebar.css +26 -0
- package/lib/public/css/sticky-notes.css +3 -0
- package/lib/public/index.html +2 -0
- package/lib/public/modules/admin.js +53 -5
- package/lib/public/modules/sidebar.js +299 -21
- package/lib/public/modules/sticky-notes.js +27 -5
- package/lib/public/modules/terminal.js +161 -25
- package/lib/sdk-bridge.js +53 -7
- package/lib/server.js +156 -17
- package/lib/sessions.js +48 -7
- package/lib/terminal-manager.js +4 -2
- package/lib/terminal.js +2 -1
- package/lib/users.js +92 -0
- package/package.json +1 -1
package/lib/daemon.js
CHANGED
|
@@ -38,6 +38,20 @@ try {
|
|
|
38
38
|
process.exit(1);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// --- OS users mode: check required system dependencies ---
|
|
42
|
+
if (config.osUsers) {
|
|
43
|
+
var { execSync: checkExec } = require("child_process");
|
|
44
|
+
var missing = [];
|
|
45
|
+
try { checkExec("which setfacl", { stdio: "ignore" }); } catch (e) { missing.push("acl (setfacl)"); }
|
|
46
|
+
try { checkExec("which git", { stdio: "ignore" }); } catch (e) { missing.push("git"); }
|
|
47
|
+
try { checkExec("which useradd", { stdio: "ignore" }); } catch (e) { missing.push("useradd"); }
|
|
48
|
+
if (missing.length > 0) {
|
|
49
|
+
console.error("[daemon] OS users mode requires missing system packages: " + missing.join(", "));
|
|
50
|
+
console.error("[daemon] Install with: sudo apt install " + missing.map(function (m) { return m.split(" ")[0]; }).join(" "));
|
|
51
|
+
process.exit(78); // EX_CONFIG
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
// --- TLS ---
|
|
42
56
|
var tlsOptions = null;
|
|
43
57
|
if (config.tls) {
|
|
@@ -82,6 +96,18 @@ var lanIp = (function () {
|
|
|
82
96
|
return null;
|
|
83
97
|
})();
|
|
84
98
|
|
|
99
|
+
// --- Helper: get removed projects filtered by existing paths and userId ---
|
|
100
|
+
function getFilteredRemovedProjects(userId) {
|
|
101
|
+
if (!config.removedProjects || config.removedProjects.length === 0) return [];
|
|
102
|
+
return config.removedProjects.filter(function (rp) {
|
|
103
|
+
// In single-user mode (no userId), show entries with no userId
|
|
104
|
+
// In multi-user mode, only show entries belonging to this user
|
|
105
|
+
if (userId && rp.userId && rp.userId !== userId) return false;
|
|
106
|
+
if (!userId && rp.userId) return false;
|
|
107
|
+
return fs.existsSync(rp.path);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
85
111
|
// --- Create multi-project server ---
|
|
86
112
|
var listenHost = config.host || "0.0.0.0";
|
|
87
113
|
|
|
@@ -94,7 +120,8 @@ var relay = createServer({
|
|
|
94
120
|
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
95
121
|
osUsers: config.osUsers || false,
|
|
96
122
|
lanHost: lanIp ? lanIp + ":" + config.port : null,
|
|
97
|
-
|
|
123
|
+
getRemovedProjects: function (userId) { return getFilteredRemovedProjects(userId); },
|
|
124
|
+
onAddProject: function (absPath, wsUser) {
|
|
98
125
|
// Check if already registered
|
|
99
126
|
for (var j = 0; j < config.projects.length; j++) {
|
|
100
127
|
if (config.projects[j].path === absPath) {
|
|
@@ -104,7 +131,17 @@ var relay = createServer({
|
|
|
104
131
|
var slugs = config.projects.map(function (p) { return p.slug; });
|
|
105
132
|
var slug = generateSlug(absPath, slugs);
|
|
106
133
|
relay.addProject(absPath, slug);
|
|
107
|
-
|
|
134
|
+
var projectEntry = { path: absPath, slug: slug, addedAt: Date.now() };
|
|
135
|
+
// Non-admin users own their projects and they default to private
|
|
136
|
+
if (wsUser && wsUser.id && wsUser.role !== "admin") {
|
|
137
|
+
projectEntry.ownerId = wsUser.id;
|
|
138
|
+
projectEntry.visibility = "private";
|
|
139
|
+
}
|
|
140
|
+
config.projects.push(projectEntry);
|
|
141
|
+
// Remove from removedProjects if present
|
|
142
|
+
if (config.removedProjects) {
|
|
143
|
+
config.removedProjects = config.removedProjects.filter(function (rp) { return rp.path !== absPath; });
|
|
144
|
+
}
|
|
108
145
|
saveConfig(config);
|
|
109
146
|
try { syncClayrc(config.projects); } catch (e) {}
|
|
110
147
|
console.log("[daemon] Added project (web):", slug, "→", absPath);
|
|
@@ -127,6 +164,7 @@ var relay = createServer({
|
|
|
127
164
|
return { ok: true, slug: slug };
|
|
128
165
|
},
|
|
129
166
|
onCreateProject: function (projectName, wsUser) {
|
|
167
|
+
console.log("[daemon] onCreateProject wsUser:", JSON.stringify(wsUser ? { id: wsUser.id, role: wsUser.role, username: wsUser.username, linuxUser: wsUser.linuxUser } : null));
|
|
130
168
|
var os = require("os");
|
|
131
169
|
var { execSync } = require("child_process");
|
|
132
170
|
var baseDir;
|
|
@@ -152,8 +190,9 @@ var relay = createServer({
|
|
|
152
190
|
uidGid = { uid: parseInt(passwdLine[0], 10), gid: parseInt(passwdLine[1], 10) };
|
|
153
191
|
} catch (e) {}
|
|
154
192
|
if (uidGid) {
|
|
155
|
-
|
|
193
|
+
fs.chmodSync(targetDir, 0o700);
|
|
156
194
|
execSync("chown -R " + linuxUser + ":" + linuxUser + " " + JSON.stringify(targetDir));
|
|
195
|
+
execSync("git init", { cwd: targetDir, uid: uidGid.uid, gid: uidGid.gid, env: { PATH: "/usr/local/bin:/usr/bin:/bin" } });
|
|
157
196
|
} else {
|
|
158
197
|
execSync("git init", { cwd: targetDir });
|
|
159
198
|
}
|
|
@@ -169,17 +208,25 @@ var relay = createServer({
|
|
|
169
208
|
}
|
|
170
209
|
// Register project
|
|
171
210
|
var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now() };
|
|
172
|
-
if (
|
|
173
|
-
|
|
211
|
+
if (wsUser && wsUser.id) {
|
|
212
|
+
if (config.osUsers || wsUser.role !== "admin") {
|
|
213
|
+
projectEntry.ownerId = wsUser.id;
|
|
214
|
+
}
|
|
215
|
+
if (wsUser.role !== "admin") {
|
|
216
|
+
projectEntry.visibility = "private";
|
|
217
|
+
}
|
|
174
218
|
}
|
|
175
219
|
relay.addProject(targetDir, slug);
|
|
176
220
|
config.projects.push(projectEntry);
|
|
177
221
|
saveConfig(config);
|
|
178
222
|
try { syncClayrc(config.projects); } catch (e) {}
|
|
179
|
-
console.log("[daemon] Created project:", slug, "→", targetDir);
|
|
223
|
+
console.log("[daemon] Created project:", slug, "→", targetDir, "entry:", JSON.stringify({ ownerId: projectEntry.ownerId, visibility: projectEntry.visibility }));
|
|
180
224
|
// OS users mode: grant ACL
|
|
181
225
|
if (config.osUsers && wsUser && wsUser.linuxUser) {
|
|
226
|
+
console.log("[daemon] Granting ACL:", targetDir, "→", wsUser.linuxUser);
|
|
182
227
|
grantProjectAccess(targetDir, wsUser.linuxUser);
|
|
228
|
+
} else if (config.osUsers) {
|
|
229
|
+
console.log("[daemon] Skipping ACL grant: osUsers=true but linuxUser=", wsUser && wsUser.linuxUser);
|
|
183
230
|
}
|
|
184
231
|
relay.broadcastAll({
|
|
185
232
|
type: "projects_updated",
|
|
@@ -227,14 +274,22 @@ var relay = createServer({
|
|
|
227
274
|
callback({ ok: false, error: errMsg });
|
|
228
275
|
return;
|
|
229
276
|
}
|
|
230
|
-
// chown if osUsers
|
|
277
|
+
// chown and restrict permissions if osUsers
|
|
231
278
|
if (config.osUsers && wsUser && wsUser.linuxUser) {
|
|
232
|
-
try {
|
|
279
|
+
try {
|
|
280
|
+
fs.chmodSync(targetDir, 0o700);
|
|
281
|
+
execSync("chown -R " + wsUser.linuxUser + ":" + wsUser.linuxUser + " " + JSON.stringify(targetDir));
|
|
282
|
+
} catch (e) {}
|
|
233
283
|
}
|
|
234
284
|
// Register project
|
|
235
285
|
var projectEntry = { path: targetDir, slug: slug, addedAt: Date.now() };
|
|
236
|
-
if (
|
|
237
|
-
|
|
286
|
+
if (wsUser && wsUser.id) {
|
|
287
|
+
if (config.osUsers || wsUser.role !== "admin") {
|
|
288
|
+
projectEntry.ownerId = wsUser.id;
|
|
289
|
+
}
|
|
290
|
+
if (wsUser.role !== "admin") {
|
|
291
|
+
projectEntry.visibility = "private";
|
|
292
|
+
}
|
|
238
293
|
}
|
|
239
294
|
relay.addProject(targetDir, slug);
|
|
240
295
|
config.projects.push(projectEntry);
|
|
@@ -257,22 +312,30 @@ var relay = createServer({
|
|
|
257
312
|
callback({ ok: false, error: "Failed to start git clone: " + err.message });
|
|
258
313
|
});
|
|
259
314
|
},
|
|
260
|
-
onRemoveProject: function (slug) {
|
|
261
|
-
var found =
|
|
315
|
+
onRemoveProject: function (slug, userId) {
|
|
316
|
+
var found = null;
|
|
262
317
|
for (var j = 0; j < config.projects.length; j++) {
|
|
263
|
-
if (config.projects[j].slug === slug) { found =
|
|
318
|
+
if (config.projects[j].slug === slug) { found = config.projects[j]; break; }
|
|
264
319
|
}
|
|
265
320
|
if (!found) return { ok: false, error: "Project not found" };
|
|
266
|
-
//
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
321
|
+
// Save to removedProjects for re-add functionality
|
|
322
|
+
if (!config.removedProjects) config.removedProjects = [];
|
|
323
|
+
config.removedProjects.push({
|
|
324
|
+
path: found.path,
|
|
325
|
+
title: found.title || null,
|
|
326
|
+
icon: found.icon || null,
|
|
327
|
+
userId: userId || null,
|
|
328
|
+
removedAt: Date.now(),
|
|
329
|
+
});
|
|
330
|
+
// Cap at 20 entries (oldest first)
|
|
331
|
+
if (config.removedProjects.length > 20) {
|
|
332
|
+
config.removedProjects = config.removedProjects.slice(config.removedProjects.length - 20);
|
|
270
333
|
}
|
|
271
334
|
relay.removeProject(slug);
|
|
272
335
|
config.projects = config.projects.filter(function (p) { return p.slug !== slug; });
|
|
273
336
|
saveConfig(config);
|
|
274
337
|
// Remove from .clayrc so it doesn't appear in restore prompt
|
|
275
|
-
if (
|
|
338
|
+
if (found.path) { try { removeFromClayrc(found.path); } catch (e) {} }
|
|
276
339
|
try { syncClayrc(config.projects); } catch (e) {}
|
|
277
340
|
console.log("[daemon] Removed project (web):", slug);
|
|
278
341
|
relay.broadcastAll({
|
|
@@ -356,6 +419,7 @@ var relay = createServer({
|
|
|
356
419
|
return { ok: true };
|
|
357
420
|
},
|
|
358
421
|
onProjectOwnerChanged: function (slug, ownerId) {
|
|
422
|
+
console.log("[daemon] onProjectOwnerChanged:", slug, "→", ownerId);
|
|
359
423
|
var oldOwnerId = null;
|
|
360
424
|
var projectIdx = -1;
|
|
361
425
|
for (var oi = 0; oi < config.projects.length; oi++) {
|
|
@@ -386,6 +450,7 @@ var relay = createServer({
|
|
|
386
450
|
// Grant new owner
|
|
387
451
|
if (ownerId) {
|
|
388
452
|
var newOwner = usersModule.findUserById(ownerId);
|
|
453
|
+
console.log("[daemon] Owner grant ACL:", ownerId, "linuxUser:", newOwner && newOwner.linuxUser, "path:", projectPath);
|
|
389
454
|
if (newOwner && newOwner.linuxUser) {
|
|
390
455
|
grantProjectAccess(projectPath, newOwner.linuxUser);
|
|
391
456
|
}
|
|
@@ -669,6 +734,7 @@ var relay = createServer({
|
|
|
669
734
|
slug: slug,
|
|
670
735
|
visibility: config.projects[i].visibility || "public",
|
|
671
736
|
allowedUsers: config.projects[i].allowedUsers || [],
|
|
737
|
+
ownerId: config.projects[i].ownerId || null,
|
|
672
738
|
};
|
|
673
739
|
}
|
|
674
740
|
}
|
|
@@ -1052,25 +1118,31 @@ if (config.keepAwake && process.platform === "darwin") {
|
|
|
1052
1118
|
|
|
1053
1119
|
// --- Spawn new daemon and graceful restart ---
|
|
1054
1120
|
function spawnAndRestart() {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1121
|
+
try {
|
|
1122
|
+
var { spawn: spawnRestart } = require("child_process");
|
|
1123
|
+
var { logPath: restartLogPath, configPath: restartConfigPath } = require("./config");
|
|
1124
|
+
var daemonScript = path.join(__dirname, "daemon.js");
|
|
1125
|
+
var logFd = fs.openSync(restartLogPath(), "a");
|
|
1126
|
+
var child = spawnRestart(process.execPath, [daemonScript], {
|
|
1127
|
+
detached: true,
|
|
1128
|
+
windowsHide: true,
|
|
1129
|
+
stdio: ["ignore", logFd, logFd],
|
|
1130
|
+
env: Object.assign({}, process.env, {
|
|
1131
|
+
CLAY_CONFIG: restartConfigPath(),
|
|
1132
|
+
}),
|
|
1133
|
+
});
|
|
1134
|
+
child.unref();
|
|
1135
|
+
fs.closeSync(logFd);
|
|
1136
|
+
config.pid = child.pid;
|
|
1137
|
+
saveConfig(config);
|
|
1138
|
+
console.log("[daemon] Spawned new daemon (PID " + child.pid + "), shutting down...");
|
|
1139
|
+
updateHandoff = true;
|
|
1140
|
+
setTimeout(function () { gracefulShutdown(); }, 100);
|
|
1141
|
+
} catch (e) {
|
|
1142
|
+
console.error("[daemon] Restart failed:", e.message);
|
|
1143
|
+
relay.broadcastAll({ type: "toast", level: "error", message: "Restart failed: " + e.message });
|
|
1144
|
+
relay.broadcastAll({ type: "restart_server_result", ok: false, error: e.message });
|
|
1145
|
+
}
|
|
1074
1146
|
}
|
|
1075
1147
|
|
|
1076
1148
|
// --- Graceful shutdown ---
|
package/lib/os-users.js
CHANGED
|
@@ -174,6 +174,52 @@ function linuxUserExists(username) {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Install Claude CLI for a Linux user account.
|
|
179
|
+
* Downloads and runs the install script, then ensures PATH is configured.
|
|
180
|
+
* Non-fatal: logs errors but does not throw.
|
|
181
|
+
*/
|
|
182
|
+
function installClaudeCli(linuxName) {
|
|
183
|
+
try {
|
|
184
|
+
// Download and run the Claude CLI install script as the target user
|
|
185
|
+
execSync(
|
|
186
|
+
"su - " + linuxName + " -c \"curl -fsSL https://claude.ai/install.sh | bash\"",
|
|
187
|
+
{ encoding: "utf8", timeout: 60000, stdio: "pipe" }
|
|
188
|
+
);
|
|
189
|
+
console.log("[os-users] Claude CLI installed for " + linuxName);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
var msg = (e.stderr || e.message || "").trim();
|
|
192
|
+
console.error("[os-users] Failed to install Claude CLI for " + linuxName + ": " + msg);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Append PATH export to the user's shell config if not already present
|
|
197
|
+
try {
|
|
198
|
+
var home = "/home/" + linuxName;
|
|
199
|
+
var rcFile;
|
|
200
|
+
if (fs.existsSync(home + "/.zshrc")) {
|
|
201
|
+
rcFile = "~/.zshrc";
|
|
202
|
+
} else {
|
|
203
|
+
rcFile = "~/.bashrc";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if PATH export is already present before appending
|
|
207
|
+
var checkCmd = "su - " + linuxName + " -c \"grep -qF '/.local/bin' " + rcFile + "\"";
|
|
208
|
+
try {
|
|
209
|
+
execSync(checkCmd, { encoding: "utf8", timeout: 5000, stdio: "pipe" });
|
|
210
|
+
console.log("[os-users] PATH already configured in " + rcFile + " for " + linuxName);
|
|
211
|
+
} catch (grepErr) {
|
|
212
|
+
// grep returned non-zero, meaning the line is not present; append it
|
|
213
|
+
var appendCmd = "su - " + linuxName + " -c 'echo \"export PATH=\\\"\\$HOME/.local/bin:\\$PATH\\\"\" >> " + rcFile + "'";
|
|
214
|
+
execSync(appendCmd, { encoding: "utf8", timeout: 5000, stdio: "pipe" });
|
|
215
|
+
console.log("[os-users] PATH export appended to " + rcFile + " for " + linuxName);
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
var rcMsg = (e.stderr || e.message || "").trim();
|
|
219
|
+
console.error("[os-users] Failed to configure PATH for " + linuxName + ": " + rcMsg);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
177
223
|
/**
|
|
178
224
|
* Provision a Linux user account for a Clay user.
|
|
179
225
|
* Creates the account via useradd with a home directory.
|
|
@@ -197,6 +243,7 @@ function provisionLinuxUser(clayUsername) {
|
|
|
197
243
|
stdio: "pipe",
|
|
198
244
|
});
|
|
199
245
|
console.log("[os-users] Provisioned Linux user: " + linuxName + " (Clay user: " + clayUsername + ")");
|
|
246
|
+
installClaudeCli(linuxName);
|
|
200
247
|
return { ok: true, linuxUser: linuxName };
|
|
201
248
|
} catch (e) {
|
|
202
249
|
var msg = (e.stderr || e.message || "").trim();
|
|
@@ -219,6 +266,12 @@ function provisionAllUsers(usersModule) {
|
|
|
219
266
|
if (user.linuxUser) {
|
|
220
267
|
// Already mapped, verify the Linux user still exists
|
|
221
268
|
if (linuxUserExists(user.linuxUser)) {
|
|
269
|
+
// Ensure Claude CLI is installed for existing users
|
|
270
|
+
var cliPath = "/home/" + user.linuxUser + "/.local/bin/claude";
|
|
271
|
+
if (!fs.existsSync(cliPath)) {
|
|
272
|
+
console.log("[os-users] Claude CLI missing for " + user.linuxUser + ", installing...");
|
|
273
|
+
installClaudeCli(user.linuxUser);
|
|
274
|
+
}
|
|
222
275
|
result.skipped.push({ id: user.id, username: user.username, linuxUser: user.linuxUser });
|
|
223
276
|
continue;
|
|
224
277
|
}
|
|
@@ -282,7 +335,10 @@ function deactivateLinuxUser(linuxUsername) {
|
|
|
282
335
|
function ensureProjectsDir() {
|
|
283
336
|
var dir = "/var/clay/projects";
|
|
284
337
|
if (!fs.existsSync(dir)) {
|
|
285
|
-
fs.mkdirSync(dir, { recursive: true, mode:
|
|
338
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o711 });
|
|
339
|
+
} else {
|
|
340
|
+
// Tighten permissions if directory already exists (prevent listing by other users)
|
|
341
|
+
try { fs.chmodSync(dir, 0o711); } catch (e) {}
|
|
286
342
|
}
|
|
287
343
|
}
|
|
288
344
|
|
|
@@ -296,6 +352,7 @@ module.exports = {
|
|
|
296
352
|
provisionLinuxUser: provisionLinuxUser,
|
|
297
353
|
provisionAllUsers: provisionAllUsers,
|
|
298
354
|
grantAllUsersAccess: grantAllUsersAccess,
|
|
355
|
+
installClaudeCli: installClaudeCli,
|
|
299
356
|
deactivateLinuxUser: deactivateLinuxUser,
|
|
300
357
|
ensureProjectsDir: ensureProjectsDir,
|
|
301
358
|
};
|
package/lib/pages.js
CHANGED
|
@@ -3,13 +3,21 @@ function pinPageHtml() {
|
|
|
3
3
|
'<meta charset="UTF-8">' +
|
|
4
4
|
'<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">' +
|
|
5
5
|
'<meta name="apple-mobile-web-app-capable" content="yes">' +
|
|
6
|
+
'<link rel="icon" type="image/png" href="/favicon-banded.png">' +
|
|
7
|
+
'<link rel="apple-touch-icon" href="/apple-touch-icon.png">' +
|
|
6
8
|
'<title>Clay</title>' +
|
|
7
9
|
'<style>' + authPageStyles + '</style></head><body><div class="c">' +
|
|
8
|
-
'<h1
|
|
10
|
+
'<h1 id="greeting"></h1>' +
|
|
9
11
|
'<div class="sub">Enter your PIN to continue</div>' +
|
|
10
12
|
pinBoxesHtml +
|
|
11
13
|
'<div class="err" id="err"></div>' +
|
|
12
14
|
'<script>' +
|
|
15
|
+
'(function(){' +
|
|
16
|
+
'var h=document.getElementById("greeting");' +
|
|
17
|
+
'var visited=localStorage.getItem("clay_visited");' +
|
|
18
|
+
'if(visited){h.textContent="Welcome back"}' +
|
|
19
|
+
'else{h.textContent="Welcome to Clay";localStorage.setItem("clay_visited","1")}' +
|
|
20
|
+
'})();' +
|
|
13
21
|
pinBoxScript +
|
|
14
22
|
'var err=document.getElementById("err");' +
|
|
15
23
|
'function submitPin(){' +
|
|
@@ -966,39 +974,33 @@ function multiUserLoginPageHtml() {
|
|
|
966
974
|
'.catch(function(){errs[1].textContent="Connection error";btns[1].disabled=false})}' +
|
|
967
975
|
'btns[1].onclick=doLogin;' +
|
|
968
976
|
|
|
969
|
-
// Force PIN change
|
|
977
|
+
// Force PIN change: reuse the same login screen, switch step1 to "Set new PIN" mode
|
|
970
978
|
'function showChangePinOverlay(){' +
|
|
971
|
-
|
|
972
|
-
'
|
|
973
|
-
|
|
974
|
-
'
|
|
975
|
-
'
|
|
976
|
-
'
|
|
977
|
-
'
|
|
978
|
-
'
|
|
979
|
-
|
|
980
|
-
'
|
|
981
|
-
'
|
|
982
|
-
'
|
|
983
|
-
'
|
|
984
|
-
|
|
985
|
-
'
|
|
986
|
-
'
|
|
987
|
-
'
|
|
988
|
-
'var errEl=ov.querySelector("#new-pin-err");' +
|
|
989
|
-
'initPinBoxes("new-pin-boxes","new-pin",function(){if(!saveBtn.disabled)doSavePin()});' +
|
|
990
|
-
'var nb=ov.querySelectorAll(".pin-digit");' +
|
|
991
|
-
'for(var i=0;i<nb.length;i++)nb[i].addEventListener("input",function(){saveBtn.disabled=newPinEl.value.length!==6});' +
|
|
992
|
-
'function doSavePin(){' +
|
|
993
|
-
'saveBtn.disabled=true;errEl.textContent="";' +
|
|
979
|
+
// Hide step dots (no longer a 2-step flow)
|
|
980
|
+
'var bar=document.querySelector(".steps-bar");if(bar)bar.style.display="none";' +
|
|
981
|
+
// Hide step 0 (username), show step 1 (PIN) with new labels
|
|
982
|
+
'steps[0].classList.remove("active");' +
|
|
983
|
+
'steps[1].classList.add("active");' +
|
|
984
|
+
'var h1=steps[1].querySelector("h1");if(h1)h1.textContent="Set your new PIN";' +
|
|
985
|
+
'var sub=steps[1].querySelector(".sub");if(sub)sub.textContent="Your temporary PIN has expired. Please set a new 6-digit PIN to continue.";' +
|
|
986
|
+
'btns[1].textContent="Save PIN";' +
|
|
987
|
+
// Re-init PIN boxes for fresh input
|
|
988
|
+
'resetPin();' +
|
|
989
|
+
'initPinBoxes("pin-boxes","pin",function(){if(!btns[1].disabled)doSaveNewPin()});' +
|
|
990
|
+
'var boxes=document.querySelectorAll(".pin-digit");' +
|
|
991
|
+
'for(var i=0;i<boxes.length;i++)boxes[i].addEventListener("input",function(){btns[1].disabled=pinEl.value.length!==6});' +
|
|
992
|
+
// Override button handler to save new PIN instead of login
|
|
993
|
+
'btns[1].onclick=doSaveNewPin;' +
|
|
994
|
+
'function doSaveNewPin(){' +
|
|
995
|
+
'btns[1].disabled=true;errs[1].textContent="";' +
|
|
994
996
|
'fetch("/api/user/pin",{method:"PUT",headers:{"Content-Type":"application/json"},' +
|
|
995
|
-
'body:JSON.stringify({newPin:
|
|
997
|
+
'body:JSON.stringify({newPin:pinEl.value})})' +
|
|
996
998
|
'.then(function(r){return r.json()})' +
|
|
997
999
|
'.then(function(d){' +
|
|
998
1000
|
'if(d.ok){location.reload();return}' +
|
|
999
|
-
'
|
|
1000
|
-
'.catch(function(){
|
|
1001
|
-
'
|
|
1001
|
+
'errs[1].textContent=d.error||"Failed to save PIN";btns[1].disabled=false})' +
|
|
1002
|
+
'.catch(function(){errs[1].textContent="Connection error";btns[1].disabled=false})}' +
|
|
1003
|
+
'}' +
|
|
1002
1004
|
|
|
1003
1005
|
'</script></div></body></html>';
|
|
1004
1006
|
}
|
package/lib/project.js
CHANGED
|
@@ -108,6 +108,7 @@ function createProjectContext(opts) {
|
|
|
108
108
|
var moveAllSchedulesToProject = opts.moveAllSchedulesToProject || function () { return { ok: false, error: "Not supported" }; };
|
|
109
109
|
var getScheduleCount = opts.getScheduleCount || function () { return 0; };
|
|
110
110
|
var onProcessingChanged = opts.onProcessingChanged || function () {};
|
|
111
|
+
var onSessionDone = opts.onSessionDone || function () {};
|
|
111
112
|
var onPresenceChange = opts.onPresenceChange || function () {};
|
|
112
113
|
var updateChannel = opts.updateChannel || "stable";
|
|
113
114
|
var osUsers = opts.osUsers || false;
|
|
@@ -176,6 +177,13 @@ function createProjectContext(opts) {
|
|
|
176
177
|
if (ws.readyState === 1) ws.send(JSON.stringify(obj));
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
function sendToAdmins(obj) {
|
|
181
|
+
var data = JSON.stringify(obj);
|
|
182
|
+
for (var ws of clients) {
|
|
183
|
+
if (ws.readyState === 1 && ws._clayUser && ws._clayUser.role === "admin") ws.send(data);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
179
187
|
function broadcastClientCount() {
|
|
180
188
|
var msg = { type: "client_count", count: clients.size };
|
|
181
189
|
if (usersModule.isMultiUser()) {
|
|
@@ -338,6 +346,7 @@ function createProjectContext(opts) {
|
|
|
338
346
|
fn(ws, filterFn);
|
|
339
347
|
}
|
|
340
348
|
},
|
|
349
|
+
onSessionDone: onSessionDone,
|
|
341
350
|
});
|
|
342
351
|
var _projMode = typeof opts.onGetProjectDefaultMode === "function" ? opts.onGetProjectDefaultMode(slug) : null;
|
|
343
352
|
var _srvMode = typeof opts.onGetServerDefaultMode === "function" ? opts.onGetServerDefaultMode() : null;
|
|
@@ -998,11 +1007,11 @@ function createProjectContext(opts) {
|
|
|
998
1007
|
var tm = createTerminalManager({ cwd: cwd, send: send, sendTo: sendTo });
|
|
999
1008
|
var nm = createNotesManager({ cwd: cwd, send: send, sendTo: sendTo });
|
|
1000
1009
|
|
|
1001
|
-
// Check for updates in background
|
|
1010
|
+
// Check for updates in background (admin only)
|
|
1002
1011
|
fetchVersion(updateChannel).then(function (v) {
|
|
1003
1012
|
if (v && isNewer(v, currentVersion)) {
|
|
1004
1013
|
latestVersion = v;
|
|
1005
|
-
|
|
1014
|
+
sendToAdmins({ type: "update_available", version: v });
|
|
1006
1015
|
}
|
|
1007
1016
|
});
|
|
1008
1017
|
|
|
@@ -1022,7 +1031,7 @@ function createProjectContext(opts) {
|
|
|
1022
1031
|
var _userId = ws._clayUser ? ws._clayUser.id : null;
|
|
1023
1032
|
var _filteredProjects = getProjectList(_userId);
|
|
1024
1033
|
sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId });
|
|
1025
|
-
if (latestVersion) {
|
|
1034
|
+
if (latestVersion && ws._clayUser && ws._clayUser.role === "admin") {
|
|
1026
1035
|
sendTo(ws, { type: "update_available", version: latestVersion });
|
|
1027
1036
|
}
|
|
1028
1037
|
if (sm.slashCommands) {
|
|
@@ -1203,7 +1212,7 @@ function createProjectContext(opts) {
|
|
|
1203
1212
|
|
|
1204
1213
|
function handleMessage(ws, msg) {
|
|
1205
1214
|
// --- DM messages (delegated to server-level handler) ---
|
|
1206
|
-
if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing") {
|
|
1215
|
+
if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing" || msg.type === "dm_add_favorite" || msg.type === "dm_remove_favorite") {
|
|
1207
1216
|
if (typeof opts.onDmMessage === "function") {
|
|
1208
1217
|
opts.onDmMessage(ws, msg);
|
|
1209
1218
|
}
|
|
@@ -1405,9 +1414,17 @@ function createProjectContext(opts) {
|
|
|
1405
1414
|
if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return;
|
|
1406
1415
|
var newChannel = msg.channel === "beta" ? "beta" : "stable";
|
|
1407
1416
|
updateChannel = newChannel;
|
|
1417
|
+
latestVersion = null;
|
|
1408
1418
|
if (typeof opts.onSetUpdateChannel === "function") {
|
|
1409
1419
|
opts.onSetUpdateChannel(newChannel);
|
|
1410
1420
|
}
|
|
1421
|
+
// Re-fetch with new channel and broadcast to admin clients
|
|
1422
|
+
fetchVersion(updateChannel).then(function (v) {
|
|
1423
|
+
if (v && isNewer(v, currentVersion)) {
|
|
1424
|
+
latestVersion = v;
|
|
1425
|
+
sendToAdmins({ type: "update_available", version: v });
|
|
1426
|
+
}
|
|
1427
|
+
}).catch(function () {});
|
|
1411
1428
|
return;
|
|
1412
1429
|
}
|
|
1413
1430
|
|
|
@@ -1815,7 +1832,7 @@ function createProjectContext(opts) {
|
|
|
1815
1832
|
new Promise(function (resolve) { setTimeout(resolve, 3000); }),
|
|
1816
1833
|
]).then(function () {
|
|
1817
1834
|
try {
|
|
1818
|
-
var newSession = sm.createSession();
|
|
1835
|
+
var newSession = sm.createSession(null, ws);
|
|
1819
1836
|
// Send the plan as the first user message (with planContent for UI rendering)
|
|
1820
1837
|
var userMsg = { type: "user_message", text: planPrompt, planContent: clientPlanContent || null };
|
|
1821
1838
|
newSession.history.push(userMsg);
|
|
@@ -1963,7 +1980,7 @@ function createProjectContext(opts) {
|
|
|
1963
1980
|
return;
|
|
1964
1981
|
}
|
|
1965
1982
|
if (typeof opts.onAddProject === "function") {
|
|
1966
|
-
var result = opts.onAddProject(addAbs);
|
|
1983
|
+
var result = opts.onAddProject(addAbs, ws._clayUser);
|
|
1967
1984
|
sendTo(ws, { type: "add_project_result", ok: result.ok, slug: result.slug, error: result.error, existing: result.existing });
|
|
1968
1985
|
} else {
|
|
1969
1986
|
sendTo(ws, { type: "add_project_result", ok: false, error: "Not supported" });
|
|
@@ -2029,8 +2046,10 @@ function createProjectContext(opts) {
|
|
|
2029
2046
|
moveAllSchedulesToProject(removeSlug, msg.moveTasksTo);
|
|
2030
2047
|
}
|
|
2031
2048
|
if (typeof opts.onRemoveProject === "function") {
|
|
2032
|
-
|
|
2033
|
-
sendTo(ws, { type: "remove_project_result", ok:
|
|
2049
|
+
// Send result before removing so the WS is still open
|
|
2050
|
+
sendTo(ws, { type: "remove_project_result", ok: true, slug: removeSlug });
|
|
2051
|
+
var removeUserId = ws._clayUser ? ws._clayUser.id : null;
|
|
2052
|
+
opts.onRemoveProject(removeSlug, removeUserId);
|
|
2034
2053
|
} else {
|
|
2035
2054
|
sendTo(ws, { type: "remove_project_result", ok: false, error: "Not supported" });
|
|
2036
2055
|
}
|