clay-server 5.1.1-beta.1 → 5.2.0-beta.2
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/lib/account-auth-policy.js +21 -0
- package/lib/account-auth-socket.js +29 -0
- package/lib/account-auth-throttle.js +44 -0
- package/lib/account-auth-transport.js +48 -0
- package/lib/account-login-page.js +28 -0
- package/lib/account-passkey-challenges.js +31 -0
- package/lib/account-passkey-page.js +89 -0
- package/lib/account-passkey-policy.js +39 -0
- package/lib/account-password-page.js +69 -0
- package/lib/account-passwords.js +36 -0
- package/lib/account-remembered.js +77 -0
- package/lib/account-session-page.js +51 -0
- package/lib/account-session-store.js +82 -0
- package/lib/daemon.js +2 -0
- package/lib/migrate-single-user.js +3 -23
- package/lib/project-connection.js +1 -1
- package/lib/project-server-settings.js +1 -0
- package/lib/project-sessions.js +3 -1
- package/lib/project-user-knowledge.js +2 -1
- package/lib/public/app.js +2 -0
- package/lib/public/auth/account-login.css +98 -0
- package/lib/public/auth/account-login.js +104 -0
- package/lib/public/css/command-palette.css +12 -33
- package/lib/public/css/user-settings-sessions.css +42 -0
- package/lib/public/index.html +17 -5
- package/lib/public/modules/account-auth-settings.js +71 -0
- package/lib/public/modules/account-transport-settings.js +51 -0
- package/lib/public/modules/app-header.js +3 -2
- package/lib/public/modules/app-messages.js +16 -4
- package/lib/public/modules/app-rendering.js +4 -3
- package/lib/public/modules/chat-render-runtime.js +8 -0
- package/lib/public/modules/clay-log-links.js +23 -0
- package/lib/public/modules/command-palette.js +94 -5
- package/lib/public/modules/markdown.js +1 -1
- package/lib/public/modules/passkey-browser.js +69 -0
- package/lib/public/modules/passkey-server-settings.js +57 -0
- package/lib/public/modules/project-logs-render.js +3 -2
- package/lib/public/modules/server-settings-controls.js +10 -0
- package/lib/public/modules/server-settings.js +7 -2
- package/lib/public/modules/user-settings-account.js +14 -1
- package/lib/public/modules/user-settings-sessions.js +120 -0
- package/lib/public/modules/user-settings.js +3 -0
- package/lib/public/modules/you-activity.js +3 -1
- package/lib/public/modules/you.js +91 -6
- package/lib/public/style.css +1 -0
- package/lib/server-account-passkeys.js +186 -0
- package/lib/server-account-passwords.js +127 -0
- package/lib/server-account-security.js +53 -0
- package/lib/server-auth.js +107 -79
- package/lib/server-home-chat.js +3 -3
- package/lib/server-home-clay-session-links.js +17 -9
- package/lib/server-settings.js +2 -1
- package/lib/server.js +3 -1
- package/lib/sessions.js +3 -1
- package/lib/user-knowledge-service.js +23 -1
- package/lib/users-auth.js +38 -3
- package/lib/users-create.js +97 -0
- package/lib/users-server-pin.js +37 -0
- package/lib/users.js +11 -84
- package/lib/workspace-query-service.js +13 -2
- package/package.json +2 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { enhanceSettingsSelects } from './settings-select.js';
|
|
3
|
+
import { confirmSettingsAction, setSettingsDraft, clearSettingsDraft } from './server-settings-state.js';
|
|
4
|
+
import { initPasskeyServerSettings, loadPasskeyServerSettings } from './passkey-server-settings.js';
|
|
5
|
+
import { initAccountTransportSettings, loadAccountTransportSettings } from './account-transport-settings.js';
|
|
6
|
+
|
|
7
|
+
function render(state) {
|
|
8
|
+
var section = document.getElementById('account-auth-policy');
|
|
9
|
+
if (!section) return;
|
|
10
|
+
var select = section.querySelector('select');
|
|
11
|
+
var save = section.querySelector('[data-policy-save]');
|
|
12
|
+
section.hidden = state.available === false;
|
|
13
|
+
select.value = state.draft || state.method || 'pin';
|
|
14
|
+
select.disabled = !!state.pending || state.method === 'password' || !state.method;
|
|
15
|
+
select.dispatchEvent(new Event('settings-sync'));
|
|
16
|
+
save.hidden = !state.method || state.method === select.value;
|
|
17
|
+
save.disabled = !!state.pending;
|
|
18
|
+
section.querySelector('[role=status]').textContent = state.error || (state.pending ? 'Saving…' : state.method === 'password' ? 'Password required. Existing account PINs only authorize password setup.' : 'Existing users verify their account PIN once, then set a password before continuing.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function loadAccountAuthSettings() {
|
|
22
|
+
loadPasskeyServerSettings();
|
|
23
|
+
var section = document.getElementById('account-auth-policy');
|
|
24
|
+
if (!section) return;
|
|
25
|
+
if ((store.get('accountAuthPolicy') || {}).draft) return;
|
|
26
|
+
try {
|
|
27
|
+
var response = await fetch('/api/account-auth-policy');
|
|
28
|
+
var data = await response.json();
|
|
29
|
+
if (!response.ok) throw new Error(data.error || 'Could not load account policy.');
|
|
30
|
+
store.set({ accountAuthPolicy: data }); render(data); loadAccountTransportSettings();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
var state = Object.assign({}, store.get('accountAuthPolicy'), { error: error.message });
|
|
33
|
+
store.set({ accountAuthPolicy: state }); render(state);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function initAccountAuthSettings() {
|
|
38
|
+
var page = document.querySelector('.server-settings-section[data-section="security"]');
|
|
39
|
+
if (!page || document.getElementById('account-auth-policy')) return;
|
|
40
|
+
var section = document.createElement('section'); section.id = 'account-auth-policy';
|
|
41
|
+
section.className = 'settings-card';
|
|
42
|
+
section.innerHTML = '<h3>Account sign-in</h3><div class="settings-field-row"><div><label class="settings-label" for="account-auth-method">Authentication method</label><p class="settings-hint">Applies to all accounts on this server. Users manage their own credentials in Personal settings.</p></div><select class="settings-select" id="account-auth-method" disabled><option value="pin">Account PIN</option><option value="password">Password / passphrase</option></select></div><p class="settings-hint" role="status">Loading account policy…</p><div class="settings-form-actions"><button class="settings-btn-sm" data-policy-save hidden>Apply password policy</button></div>';
|
|
43
|
+
var description = page.querySelector('.settings-page-description');
|
|
44
|
+
description.textContent = 'Sign-in policies and connection security for this server. Manage your own password, passkeys and devices in Personal settings → Account.';
|
|
45
|
+
description.after(section);
|
|
46
|
+
var select = section.querySelector('select');
|
|
47
|
+
select.onchange = function () {
|
|
48
|
+
var state = Object.assign({}, store.get('accountAuthPolicy'), { draft: select.value, error: '' });
|
|
49
|
+
if (state.draft === state.method) { delete state.draft; clearSettingsDraft('accountAuth'); }
|
|
50
|
+
else setSettingsDraft('accountAuth', true);
|
|
51
|
+
store.set({ accountAuthPolicy: state }); render(state);
|
|
52
|
+
};
|
|
53
|
+
section.querySelector('[data-policy-save]').onclick = function () {
|
|
54
|
+
confirmSettingsAction('Require passwords for account users? Everyone using an account, including you, must verify their account PIN and set a password before continuing. Existing PINs remain valid for setup until each password is saved. This change does not alter solo access or the Server PIN.', 'Require passwords', async function () {
|
|
55
|
+
var state = Object.assign({}, store.get('accountAuthPolicy'), { pending: true, error: '' });
|
|
56
|
+
store.set({ accountAuthPolicy: state }); render(state);
|
|
57
|
+
try {
|
|
58
|
+
var response = await fetch('/api/account-auth-policy', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ method: 'password' }) });
|
|
59
|
+
var data = await response.json();
|
|
60
|
+
if (!response.ok || !data.ok) throw new Error(data.error || 'Could not save the policy.');
|
|
61
|
+
clearSettingsDraft('accountAuth'); window.location.reload();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
state.pending = false; state.error = error.message;
|
|
64
|
+
store.set({ accountAuthPolicy: state }); render(state);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
enhanceSettingsSelects(section);
|
|
69
|
+
initPasskeyServerSettings();
|
|
70
|
+
initAccountTransportSettings();
|
|
71
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { setSettingsDraft, clearSettingsDraft } from './server-settings-state.js';
|
|
3
|
+
function render(state) {
|
|
4
|
+
var section = document.getElementById('account-transport'); if (!section) return;
|
|
5
|
+
section.hidden = !state.available;
|
|
6
|
+
if (!state.dirty) {
|
|
7
|
+
section.querySelector('[name=origin]').value = state.origin || '';
|
|
8
|
+
section.querySelector('[name=proxies]').value = (state.trustedProxies || []).join('\n');
|
|
9
|
+
}
|
|
10
|
+
section.querySelectorAll('input,textarea,button').forEach(function (element) { element.disabled = !!state.pending; });
|
|
11
|
+
section.querySelector('[role=status]').textContent = state.error || (state.pending ? 'Saving…' : state.origin ? 'Saved proxy HTTPS address: ' + state.origin : 'Direct HTTPS or localhost access. No proxy headers are trusted.');
|
|
12
|
+
}
|
|
13
|
+
export async function loadAccountTransportSettings() {
|
|
14
|
+
if ((store.get('accountTransport') || {}).dirty) return;
|
|
15
|
+
try {
|
|
16
|
+
var response = await fetch('/api/account-transport'); var data = await response.json();
|
|
17
|
+
if (!response.ok) throw new Error(data.error || 'Could not load connection settings.');
|
|
18
|
+
var state = Object.assign({}, data, { available: (store.get('accountAuthPolicy') || {}).method === 'password' });
|
|
19
|
+
store.set({ accountTransport: state }); render(state);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
var state = Object.assign({}, store.get('accountTransport'), { error: error.message });
|
|
22
|
+
store.set({ accountTransport: state }); render(state);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function initAccountTransportSettings() {
|
|
26
|
+
var anchor = document.getElementById('passkey-policy'); if (!anchor || document.getElementById('account-transport')) return;
|
|
27
|
+
var section = document.createElement('section'); section.id = 'account-transport'; section.className = 'settings-card'; section.hidden = true;
|
|
28
|
+
section.innerHTML = '<h3>HTTPS & trusted proxies</h3><p class="settings-hint">Connection security for account users on this server. Account passwords require HTTPS for remote access. Localhost is available for setup and recovery.</p>' +
|
|
29
|
+
'<form><label class="settings-label" for="account-external-origin">Proxy HTTPS address</label><input class="us-text-input" id="account-external-origin" name="origin" type="url" placeholder="https://clay.example.com">' +
|
|
30
|
+
'<label class="settings-label" for="account-trusted-proxies">Trusted proxy IP addresses</label><textarea class="settings-network-origins" id="account-trusted-proxies" name="proxies" rows="2" placeholder="One exact IP address per line"></textarea>' +
|
|
31
|
+
'<p class="settings-hint">Leave empty for direct HTTPS. Each trusted proxy must overwrite X-Forwarded-Proto and X-Real-IP, prevent direct public access to the backend, and serve this HTTPS address. Forwarded host headers are ignored.</p>' +
|
|
32
|
+
'<label class="settings-label" for="account-transport-password">Administrator password</label><input class="us-text-input" id="account-transport-password" name="password" type="password" autocomplete="current-password" required>' +
|
|
33
|
+
'<div class="settings-form-actions"><button class="settings-btn-sm">Save connection settings</button></div></form><p class="settings-hint" role="status"></p>';
|
|
34
|
+
anchor.after(section);
|
|
35
|
+
section.querySelectorAll('[name=origin],[name=proxies]').forEach(function (input) { input.oninput = function () {
|
|
36
|
+
var state = Object.assign({}, store.get('accountTransport'), { dirty: true, error: '' });
|
|
37
|
+
setSettingsDraft('accountTransport', true); store.set({ accountTransport: state }); render(state);
|
|
38
|
+
}; });
|
|
39
|
+
section.querySelector('form').onsubmit = async function (event) {
|
|
40
|
+
event.preventDefault(); var password = section.querySelector('[name=password]');
|
|
41
|
+
var state = Object.assign({}, store.get('accountTransport'), { pending: true, error: '' }); store.set({ accountTransport: state }); render(state);
|
|
42
|
+
try {
|
|
43
|
+
var response = await fetch('/api/account-transport', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
|
|
44
|
+
origin: section.querySelector('[name=origin]').value.trim(), trustedProxies: section.querySelector('[name=proxies]').value.split(/\s+/).filter(Boolean), currentPassword: password.value,
|
|
45
|
+
}) });
|
|
46
|
+
var data = await response.json(); if (!response.ok) throw new Error(data.error || 'Could not save connection settings.');
|
|
47
|
+
state = Object.assign({}, data, { available: true }); clearSettingsDraft('accountTransport');
|
|
48
|
+
} catch (error) { state.error = error.message; state.pending = false; }
|
|
49
|
+
finally { password.value = ''; store.set({ accountTransport: state }); render(state); }
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -350,6 +350,7 @@ export function prependOlderHistory(items, meta) {
|
|
|
350
350
|
|
|
351
351
|
// Save current rendering state
|
|
352
352
|
var savedMsgEl = store.get('currentMsgEl');
|
|
353
|
+
var savedReplayingHistory = store.get('replayingHistory');
|
|
353
354
|
var savedActivity = getActivityEl();
|
|
354
355
|
var savedFullText = store.get('currentFullText');
|
|
355
356
|
var savedTurnCounter = getTurnCounter();
|
|
@@ -359,7 +360,7 @@ export function prependOlderHistory(items, meta) {
|
|
|
359
360
|
var savedUsage = JSON.parse(JSON.stringify(getSessionUsage()));
|
|
360
361
|
|
|
361
362
|
// Reset to initial values for clean rendering
|
|
362
|
-
store.set({ currentMsgEl: null, currentFullText: "" });
|
|
363
|
+
store.set({ currentMsgEl: null, currentFullText: "", replayingHistory: true });
|
|
363
364
|
setActivityEl(null);
|
|
364
365
|
setTurnCounter(0);
|
|
365
366
|
resetToolState();
|
|
@@ -387,7 +388,7 @@ export function prependOlderHistory(items, meta) {
|
|
|
387
388
|
setPrependAnchor(null);
|
|
388
389
|
|
|
389
390
|
// Restore saved state
|
|
390
|
-
store.set({ currentMsgEl: savedMsgEl, currentFullText: savedFullText });
|
|
391
|
+
store.set({ currentMsgEl: savedMsgEl, currentFullText: savedFullText, replayingHistory: savedReplayingHistory });
|
|
391
392
|
setActivityEl(savedActivity);
|
|
392
393
|
setTurnCounter(savedTurnCounter);
|
|
393
394
|
restoreToolState(savedToolsState);
|
|
@@ -40,7 +40,7 @@ import { handleHomeMateMemoryState, handleHomeMateKnowledgeState, syncHomeMateSe
|
|
|
40
40
|
import { renderKnowledgeList, handleKnowledgeContent, handleKnowledgeMutation } from './mate-knowledge.js';
|
|
41
41
|
import { handleKnowledgeWorkbenchMessage } from './mate-knowledge-workbench.js';
|
|
42
42
|
import { renderMemoryList } from './mate-memory.js';
|
|
43
|
-
import { handlePaletteSessionSwitch, setPaletteVersion } from './command-palette.js';
|
|
43
|
+
import { handlePaletteSessionSwitch, handlePaletteHistoryDone, handlePaletteHistoryPrepended, setPaletteVersion } from './command-palette.js';
|
|
44
44
|
import { handleFindInSessionResults } from './session-search.js';
|
|
45
45
|
import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.js';
|
|
46
46
|
import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDelegationNotice, hideWorkerDelegationNotice } from './split-pair-ui.js';
|
|
@@ -224,6 +224,7 @@ export function processMessage(msg) {
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
store.set({ currentHistoryIndex: Number.isSafeInteger(msg._historyIndex) && msg._historyIndex >= 0 ? msg._historyIndex : null });
|
|
227
228
|
switch (msg.type) {
|
|
228
229
|
case "message_ack":
|
|
229
230
|
var acknowledgedEntry = acknowledgeMessage(msg.clientMessageId);
|
|
@@ -250,6 +251,7 @@ export function processMessage(msg) {
|
|
|
250
251
|
|
|
251
252
|
case "history_prepend":
|
|
252
253
|
prependOlderHistory(msg.items, msg.meta);
|
|
254
|
+
handlePaletteHistoryPrepended();
|
|
253
255
|
break;
|
|
254
256
|
|
|
255
257
|
case "history_done":
|
|
@@ -311,11 +313,16 @@ export function processMessage(msg) {
|
|
|
311
313
|
// re-pins on every height change while armed. Disarms early on
|
|
312
314
|
// any real user scroll input.
|
|
313
315
|
// Skip arming when we have a pending in-conversation navigate
|
|
314
|
-
// target (file-edit deeplink) — the navigate block
|
|
315
|
-
// that element into view, and sticky-bottom would
|
|
316
|
+
// target (file-edit or Session-entry deeplink) — the navigate block
|
|
317
|
+
// below scrolls that element into view, and sticky-bottom would
|
|
318
|
+
// fight it.
|
|
316
319
|
var nav = getPendingNavigate();
|
|
317
320
|
var hasNavTarget = nav && (nav.toolId || nav.assistantUuid);
|
|
318
|
-
|
|
321
|
+
var sessionEntryNav = store.get('pendingSessionEntryNavigation');
|
|
322
|
+
var hasSessionEntryNav = sessionEntryNav
|
|
323
|
+
&& String(sessionEntryNav.projectSlug) === String(store.get('currentSlug'))
|
|
324
|
+
&& String(sessionEntryNav.sessionId) === String(store.get('activeSessionId'));
|
|
325
|
+
if (hasNavTarget || hasSessionEntryNav) {
|
|
319
326
|
// Navigate block below will scrollIntoView on the target — don't
|
|
320
327
|
// arm sticky-bottom or it would fight that scroll.
|
|
321
328
|
scrollToBottom();
|
|
@@ -343,6 +350,7 @@ export function processMessage(msg) {
|
|
|
343
350
|
}
|
|
344
351
|
});
|
|
345
352
|
}
|
|
353
|
+
requestAnimationFrame(function () { handlePaletteHistoryDone(); });
|
|
346
354
|
break;
|
|
347
355
|
|
|
348
356
|
case "restore_mate_dm":
|
|
@@ -1483,6 +1491,10 @@ export function processMessage(msg) {
|
|
|
1483
1491
|
addContextOverflowMessage(msg);
|
|
1484
1492
|
break;
|
|
1485
1493
|
|
|
1494
|
+
case "account_auth_required":
|
|
1495
|
+
window.location.reload();
|
|
1496
|
+
break;
|
|
1497
|
+
|
|
1486
1498
|
case "auth_required":
|
|
1487
1499
|
removeMatePreThinking();
|
|
1488
1500
|
setActivity(null);
|
|
@@ -8,10 +8,10 @@ import { refreshIcons } from './icons.js';
|
|
|
8
8
|
import { closeToolGroup } from './tools.js';
|
|
9
9
|
import { sendMessage, hasSendableContent } from './input.js';
|
|
10
10
|
import { getChatLayout } from './theme.js';
|
|
11
|
-
import { addToMessages, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, getActivityEl } from './chat-render-runtime.js';
|
|
11
|
+
import { addToMessages, addHistoryIndex, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, getActivityEl } from './chat-render-runtime.js';
|
|
12
12
|
import { resolveProjectAssistantIdentity, applyProjectAssistantIdentity } from './project-chat-identity.js';
|
|
13
13
|
export { VENDOR_ORDER, EXPERIMENTAL_VENDORS, isExperimentalVendor } from './vendor-priority.js';
|
|
14
|
-
export { initRendering, addToMessages, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, setTurnCounter, getPrependAnchor, setPrependAnchor, getActivityEl, setActivityEl, getIsUserScrolledUp, setIsUserScrolledUp, getStickyBottom, armStickyBottom, disarmStickyBottom, VENDOR_AVATARS, VENDOR_NAMES, VENDOR_HOMEPAGES } from './chat-render-runtime.js';
|
|
14
|
+
export { initRendering, addToMessages, addHistoryIndex, scrollToBottom, forceScrollToBottom, getMsgTime, shouldGroupMessage, getTurnCounter, setTurnCounter, getPrependAnchor, setPrependAnchor, getActivityEl, setActivityEl, getIsUserScrolledUp, setIsUserScrolledUp, getStickyBottom, armStickyBottom, disarmStickyBottom, VENDOR_AVATARS, VENDOR_NAMES, VENDOR_HOMEPAGES } from './chat-render-runtime.js';
|
|
15
15
|
export { addUserMessage, addSystemMessage, addConflictMessage, addContextOverflowMessage } from './app-message-cards.js';
|
|
16
16
|
|
|
17
17
|
// --- Module-owned state (not in store) ---
|
|
@@ -44,7 +44,8 @@ export function addCopyHandler(msgEl, rawText) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export function appendDelta(text) {
|
|
47
|
-
ensureAssistantBlock();
|
|
47
|
+
var assistantEl = ensureAssistantBlock();
|
|
48
|
+
if (store.get('replayingHistory')) addHistoryIndex(assistantEl, store.get('currentHistoryIndex'));
|
|
48
49
|
streamBuffer += text;
|
|
49
50
|
if (store.get('replayingHistory')) {
|
|
50
51
|
store.set({ currentFullText: store.get('currentFullText') + streamBuffer });
|
|
@@ -155,11 +155,19 @@ export function setIsUserScrolledUp(value) { isUserScrolledUp = value; }
|
|
|
155
155
|
|
|
156
156
|
export function addToMessages(element) {
|
|
157
157
|
var messagesEl = getMessagesEl();
|
|
158
|
+
addHistoryIndex(element, store.get('currentHistoryIndex'));
|
|
158
159
|
if (prependAnchor) messagesEl.insertBefore(element, prependAnchor);
|
|
159
160
|
else messagesEl.appendChild(element);
|
|
160
161
|
keepActiveThinkingAtTail();
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
export function addHistoryIndex(element, historyIndex) {
|
|
165
|
+
if (!element || !Number.isSafeInteger(historyIndex) || historyIndex < 0) return;
|
|
166
|
+
var existing = element.dataset.historyIndexes || "";
|
|
167
|
+
if (existing.split(",").indexOf(String(historyIndex)) !== -1) return;
|
|
168
|
+
element.dataset.historyIndexes = existing ? existing + "," + historyIndex : String(historyIndex);
|
|
169
|
+
}
|
|
170
|
+
|
|
163
171
|
// Thinking is a single accumulating record for the active turn. Other live
|
|
164
172
|
// transcript entries may arrive between reasoning segments, so keep only the
|
|
165
173
|
// currently active record after the tail activity indicator.
|
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
import { enhanceClayIssueLinks, isExactClayIssueReference } from './clay-issue-links.js';
|
|
2
2
|
import { enhanceGithubLinks } from './github-links.js';
|
|
3
|
+
import { escapeHtml } from './utils.js';
|
|
3
4
|
|
|
4
5
|
export function renderClayRecordLink(href, text) {
|
|
6
|
+
if (isExactClaySessionReference(href)) return renderClaySessionLink(href.trim(), text);
|
|
5
7
|
if (!isExactClayLogReference(href) && !isExactClayIssueReference(href)) return null;
|
|
6
8
|
return '<a href="#" data-clay-record-ref="' + href.trim() + '">' + text + '</a>';
|
|
7
9
|
}
|
|
10
|
+
|
|
11
|
+
export function isExactClaySessionReference(text) {
|
|
12
|
+
return parseClaySessionReference(text) !== null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function renderClaySessionLink(ref, text) {
|
|
16
|
+
var parsed = parseClaySessionReference(ref);
|
|
17
|
+
if (!parsed) return null;
|
|
18
|
+
var label = text ? String(text).trim() : "";
|
|
19
|
+
var safeLabel = escapeHtml(label);
|
|
20
|
+
var entryAttr = parsed.entryIndex === null ? "" : ' data-session-entry-index="' + parsed.entryIndex + '"';
|
|
21
|
+
return '<button type="button" class="clayos-session-link clayos-record-link" data-session-ref="' + parsed.ref + '"' + entryAttr + ' aria-label="Open session' + (safeLabel ? " from " + safeLabel : "") + '"><span>Session</span>' + (safeLabel ? '<small>' + safeLabel + '</small>' : '') + '</button>';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseClaySessionReference(text) {
|
|
25
|
+
var match = /^((?:session:[A-Za-z0-9_-]{24}))(?:#entry-([0-9]+))?$/.exec((text || "").trim());
|
|
26
|
+
if (!match) return null;
|
|
27
|
+
var entryIndex = match[2] === undefined ? null : Number(match[2]);
|
|
28
|
+
if (entryIndex !== null && (!Number.isSafeInteger(entryIndex) || entryIndex < 0)) return null;
|
|
29
|
+
return { ref: match[1], entryIndex: entryIndex };
|
|
30
|
+
}
|
|
8
31
|
export function parseClayLogReferences(text) {
|
|
9
32
|
var pattern = /\[clayos\/(log:[A-Za-z0-9_-]{24})(?:\s+[\u2014-]\s+([^\]\n]{1,80}))?\]|(^|[^A-Za-z0-9_/:])(log:[A-Za-z0-9_-]{24})(?![A-Za-z0-9_-])/g;
|
|
10
33
|
var results = [];
|
|
@@ -5,6 +5,8 @@ import { openSearch as openSessionSearch } from './session-search.js';
|
|
|
5
5
|
import { startSearchClayChat, attachSearchClayChat, detachSearchClayChat, getSearchClayChatSummary } from './search-clay-chat.js';
|
|
6
6
|
import { getWs } from './ws-ref.js';
|
|
7
7
|
import { openProjectLog } from './project-logs.js';
|
|
8
|
+
import { store } from './store.js';
|
|
9
|
+
import { disarmStickyBottom } from './chat-render-runtime.js';
|
|
8
10
|
|
|
9
11
|
function formatRelativeDate(ts) {
|
|
10
12
|
if (!ts) return "";
|
|
@@ -70,6 +72,10 @@ function handleClaySessionLinkClick(event) {
|
|
|
70
72
|
var requestId = "clayos-" + (isLog ? "log-" : "session-") + Date.now() + "-" + Math.random().toString(36).slice(2, 9);
|
|
71
73
|
if (isLog) link.dataset.logRequest = requestId;
|
|
72
74
|
else link.dataset.sessionRequest = requestId;
|
|
75
|
+
if (!isLog) store.set({ sessionNavigationRequestId: requestId, sessionNavigationRequestContext: {
|
|
76
|
+
projectSlug: store.get('currentSlug'),
|
|
77
|
+
sessionId: store.get('activeSessionId')
|
|
78
|
+
} });
|
|
73
79
|
link.classList.remove("is-error");
|
|
74
80
|
link.classList.add("is-loading");
|
|
75
81
|
var request = { requestId: requestId, surface: link.closest(".search-clay-chat") ? "search" : "home" };
|
|
@@ -79,6 +85,7 @@ function handleClaySessionLinkClick(event) {
|
|
|
79
85
|
} else {
|
|
80
86
|
request.type = "home_clay_session_resolve";
|
|
81
87
|
request.sessionRef = link.dataset.sessionRef;
|
|
88
|
+
if (link.dataset.sessionEntryIndex !== undefined) request.entryIndex = Number(link.dataset.sessionEntryIndex);
|
|
82
89
|
}
|
|
83
90
|
ws.send(JSON.stringify(request));
|
|
84
91
|
}
|
|
@@ -91,12 +98,31 @@ export function handleClaySessionTarget(msg) {
|
|
|
91
98
|
for (var i = 0; i < links.length; i++) {
|
|
92
99
|
if ((isLog ? links[i].dataset.logRequest : links[i].dataset.sessionRequest) === msg.requestId) link = links[i];
|
|
93
100
|
}
|
|
101
|
+
if (!isLog && store.get('sessionNavigationRequestId') !== msg.requestId) {
|
|
102
|
+
if (link) {
|
|
103
|
+
link.classList.remove("is-loading");
|
|
104
|
+
delete link.dataset.sessionRequest;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
if (!isLog) {
|
|
109
|
+
var requestContext = store.get('sessionNavigationRequestContext');
|
|
110
|
+
if (requestContext && (String(store.get('currentSlug')) !== String(requestContext.projectSlug) || String(store.get('activeSessionId')) !== String(requestContext.sessionId))) {
|
|
111
|
+
if (link) {
|
|
112
|
+
link.classList.remove("is-loading");
|
|
113
|
+
delete link.dataset.sessionRequest;
|
|
114
|
+
}
|
|
115
|
+
store.set({ pendingSessionEntryNavigation: null, sessionNavigationRequestContext: null });
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
94
119
|
if (link) {
|
|
95
120
|
link.classList.remove("is-loading");
|
|
96
121
|
if (isLog) delete link.dataset.logRequest;
|
|
97
122
|
else delete link.dataset.sessionRequest;
|
|
98
123
|
}
|
|
99
124
|
if (msg.status !== "ready" || !msg.target) {
|
|
125
|
+
if (!isLog) store.set({ pendingSessionEntryNavigation: null });
|
|
100
126
|
if (link) {
|
|
101
127
|
link.classList.add("is-error");
|
|
102
128
|
link.title = msg.error || "Item not available";
|
|
@@ -108,7 +134,7 @@ export function handleClaySessionTarget(msg) {
|
|
|
108
134
|
navigateToLog(msg.target);
|
|
109
135
|
return true;
|
|
110
136
|
}
|
|
111
|
-
navigateToSession({ projectSlug: msg.target.projectSlug, sessionId: msg.target.sessionId }, null);
|
|
137
|
+
navigateToSession({ projectSlug: msg.target.projectSlug, sessionId: msg.target.sessionId }, null, msg.target.entryIndex);
|
|
112
138
|
return true;
|
|
113
139
|
}
|
|
114
140
|
|
|
@@ -151,9 +177,61 @@ export function handlePaletteSessionSwitch() {
|
|
|
151
177
|
var nav = pendingNav;
|
|
152
178
|
pendingNav = null;
|
|
153
179
|
if (ctx.currentSlug && ctx.currentSlug() === nav.slug) {
|
|
154
|
-
|
|
155
|
-
|
|
180
|
+
navigateToSession({ projectSlug: nav.slug, sessionId: nav.sessionId }, nav.query, nav.entryIndex);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function handlePaletteHistoryDone() {
|
|
185
|
+
var pending = store.get('pendingSessionEntryNavigation');
|
|
186
|
+
if (!pending) return;
|
|
187
|
+
if (!isActiveEntryNavigation(pending)) {
|
|
188
|
+
store.set({ pendingSessionEntryNavigation: null });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
var historyFrom = store.get('historyFrom') || 0;
|
|
192
|
+
if (pending.entryIndex < historyFrom) {
|
|
193
|
+
var ws = getWs();
|
|
194
|
+
if (ws && ws.readyState === 1) ws.send(JSON.stringify({ type: "load_more_history", before: historyFrom, target: pending.entryIndex }));
|
|
195
|
+
return;
|
|
156
196
|
}
|
|
197
|
+
jumpToSessionEntry(pending.entryIndex);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function handlePaletteHistoryPrepended() {
|
|
201
|
+
var pending = store.get('pendingSessionEntryNavigation');
|
|
202
|
+
if (!pending) return;
|
|
203
|
+
if (!isActiveEntryNavigation(pending)) {
|
|
204
|
+
store.set({ pendingSessionEntryNavigation: null });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
requestAnimationFrame(function () {
|
|
208
|
+
if (store.get('pendingSessionEntryNavigation') === pending) jumpToSessionEntry(pending.entryIndex, pending);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isActiveEntryNavigation(pending) {
|
|
213
|
+
return String(store.get('currentSlug') || (ctx && ctx.currentSlug ? ctx.currentSlug() : "")) === String(pending.projectSlug) && String(store.get('activeSessionId')) === String(pending.sessionId);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function jumpToSessionEntry(entryIndex, pending) {
|
|
217
|
+
if (pending && !isActiveEntryNavigation(pending)) return;
|
|
218
|
+
var messages = document.querySelectorAll("#messages [data-history-index], #messages [data-history-indexes]");
|
|
219
|
+
var target = null;
|
|
220
|
+
for (var i = 0; i < messages.length; i++) {
|
|
221
|
+
var indexes = (messages[i].dataset.historyIndexes || messages[i].dataset.historyIndex || "").split(",");
|
|
222
|
+
if (indexes.indexOf(String(entryIndex)) !== -1) { target = messages[i]; break; }
|
|
223
|
+
}
|
|
224
|
+
store.set({ pendingSessionEntryNavigation: null });
|
|
225
|
+
if (!target) return;
|
|
226
|
+
disarmStickyBottom();
|
|
227
|
+
var oldTargets = document.querySelectorAll("#messages .session-entry-target");
|
|
228
|
+
for (var oldIndex = 0; oldIndex < oldTargets.length; oldIndex++) oldTargets[oldIndex].classList.remove("session-entry-target");
|
|
229
|
+
var reducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
230
|
+
target.scrollIntoView({ behavior: reducedMotion ? "auto" : "smooth", block: "center" });
|
|
231
|
+
target.classList.remove("session-entry-target");
|
|
232
|
+
void target.offsetWidth;
|
|
233
|
+
target.classList.add("session-entry-target");
|
|
234
|
+
setTimeout(function () { target.classList.remove("session-entry-target"); }, reducedMotion ? 700 : 1800);
|
|
157
235
|
}
|
|
158
236
|
|
|
159
237
|
function navigateToLog(target) {
|
|
@@ -357,13 +435,24 @@ function activateItem(entry) {
|
|
|
357
435
|
if (entry.type === "mate-session" || entry.type === "session") navigateToSession(entry.data, entry.query);
|
|
358
436
|
}
|
|
359
437
|
|
|
360
|
-
function navigateToSession(item, query) {
|
|
438
|
+
function navigateToSession(item, query, entryIndex) {
|
|
361
439
|
closeCommandPalette();
|
|
440
|
+
pendingNav = null;
|
|
441
|
+
var hasEntry = entryIndex !== null && entryIndex !== undefined && Number.isSafeInteger(Number(entryIndex)) && Number(entryIndex) >= 0;
|
|
442
|
+
store.set({ pendingSessionEntryNavigation: hasEntry ? { projectSlug: item.projectSlug, sessionId: item.sessionId, entryIndex: Number(entryIndex) } : null });
|
|
362
443
|
if (ctx.currentSlug && ctx.currentSlug() === item.projectSlug) {
|
|
444
|
+
var alreadyActive = String(store.get('activeSessionId')) === String(item.sessionId);
|
|
445
|
+
if (alreadyActive) {
|
|
446
|
+
if (query) setTimeout(function () { openSessionSearch(query); }, 400);
|
|
447
|
+
if (hasEntry) requestAnimationFrame(function () {
|
|
448
|
+
if (!store.get('replayingHistory')) handlePaletteHistoryDone();
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
363
452
|
ctx.selectSession(item.sessionId);
|
|
364
453
|
if (query) setTimeout(function () { openSessionSearch(query); }, 400);
|
|
365
454
|
} else {
|
|
366
|
-
pendingNav = { slug: item.projectSlug, sessionId: item.sessionId, query: query };
|
|
455
|
+
pendingNav = { slug: item.projectSlug, sessionId: item.sessionId, query: query, entryIndex: hasEntry ? Number(entryIndex) : null };
|
|
367
456
|
ctx.switchProject(item.projectSlug);
|
|
368
457
|
}
|
|
369
458
|
}
|
|
@@ -84,7 +84,7 @@ export function enhanceClaySessionLinks(root) {
|
|
|
84
84
|
fragment.appendChild(document.createTextNode(text.slice(offset, match.index)));
|
|
85
85
|
var button = document.createElement("button");
|
|
86
86
|
button.type = "button";
|
|
87
|
-
button.className = "clayos-session-link";
|
|
87
|
+
button.className = "clayos-session-link clayos-record-link";
|
|
88
88
|
button.dataset.sessionRef = match[1];
|
|
89
89
|
button.setAttribute("aria-label", "Open session" + (match[2] ? " from " + match[2].trim() : ""));
|
|
90
90
|
var label = document.createElement("span");
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
function decode(value) {
|
|
2
|
+
var base64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
3
|
+
return Uint8Array.from(atob(base64), function (character) { return character.charCodeAt(0); });
|
|
4
|
+
}
|
|
5
|
+
function encode(value) {
|
|
6
|
+
var bytes = new Uint8Array(value); var binary = '';
|
|
7
|
+
bytes.forEach(function (byte) { binary += String.fromCharCode(byte); });
|
|
8
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
9
|
+
}
|
|
10
|
+
function serialize(credential) {
|
|
11
|
+
if (!credential) throw new Error('No passkey was selected.');
|
|
12
|
+
var response = { clientDataJSON: encode(credential.response.clientDataJSON) };
|
|
13
|
+
['attestationObject', 'authenticatorData', 'signature', 'userHandle'].forEach(function (key) {
|
|
14
|
+
if (credential.response[key]) response[key] = encode(credential.response[key]);
|
|
15
|
+
});
|
|
16
|
+
if (credential.response.getTransports) response.transports = credential.response.getTransports();
|
|
17
|
+
return { id: credential.id, rawId: encode(credential.rawId), type: credential.type, response: response,
|
|
18
|
+
clientExtensionResults: credential.getClientExtensionResults(), authenticatorAttachment: credential.authenticatorAttachment };
|
|
19
|
+
}
|
|
20
|
+
function prepare(options) {
|
|
21
|
+
var publicKey = Object.assign({}, options, { challenge: decode(options.challenge) });
|
|
22
|
+
['allowCredentials', 'excludeCredentials'].forEach(function (field) {
|
|
23
|
+
if (options[field]) publicKey[field] = options[field].map(function (key) { return Object.assign({}, key, { id: decode(key.id) }); });
|
|
24
|
+
});
|
|
25
|
+
if (options.user) publicKey.user = Object.assign({}, options.user, { id: decode(options.user.id) });
|
|
26
|
+
return publicKey;
|
|
27
|
+
}
|
|
28
|
+
export function supported() { return !!(window.isSecureContext && window.PublicKeyCredential && navigator.credentials); }
|
|
29
|
+
export function passkeyError(error) {
|
|
30
|
+
if (error.name === 'NotAllowedError' || error.name === 'AbortError') return 'Passkey request cancelled or timed out. You can try again or use your password.';
|
|
31
|
+
if (error.name === 'InvalidStateError') return 'This passkey is already registered. Choose a different device or passkey.';
|
|
32
|
+
return error.message || 'Could not use this passkey.';
|
|
33
|
+
}
|
|
34
|
+
export async function createPasskey(options) {
|
|
35
|
+
if (!supported()) throw new Error('Passkeys need a supported browser and a secure connection.');
|
|
36
|
+
return serialize(await navigator.credentials.create({ publicKey: prepare(options) }));
|
|
37
|
+
}
|
|
38
|
+
export async function getPasskey(options) {
|
|
39
|
+
if (!supported()) throw new Error('Passkeys need a supported browser and a secure connection.');
|
|
40
|
+
return serialize(await navigator.credentials.get({ publicKey: prepare(options) }));
|
|
41
|
+
}
|
|
42
|
+
export async function request(url, data) {
|
|
43
|
+
var response = await fetch(url, data === undefined ? { credentials: 'same-origin' } : {
|
|
44
|
+
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data),
|
|
45
|
+
});
|
|
46
|
+
var result = await response.json();
|
|
47
|
+
if (!response.ok) throw new Error(result.error || 'Could not complete this request.');
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
export async function initPasskeyLogin() {
|
|
51
|
+
var button = document.getElementById('passkey-login');
|
|
52
|
+
if (!button || !supported()) return;
|
|
53
|
+
try {
|
|
54
|
+
var status = await request('/auth/passkeys/status');
|
|
55
|
+
if (status.origin !== location.origin || document.getElementById('mode-row').hidden) return;
|
|
56
|
+
button.hidden = false;
|
|
57
|
+
button.onclick = async function () {
|
|
58
|
+
button.disabled = true;
|
|
59
|
+
var error = document.getElementById('error'); error.textContent = '';
|
|
60
|
+
try {
|
|
61
|
+
var options = await request('/auth/passkeys/login/options', {});
|
|
62
|
+
var response = await getPasskey(options.options);
|
|
63
|
+
await request('/auth/passkeys/login/verify', { response: response });
|
|
64
|
+
location.href = '/';
|
|
65
|
+
} catch (failure) { error.textContent = passkeyError(failure); }
|
|
66
|
+
finally { button.disabled = false; }
|
|
67
|
+
};
|
|
68
|
+
} catch (failure) { /* Password sign-in remains available when passkeys cannot load. */ }
|
|
69
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { setSettingsDraft, clearSettingsDraft } from './server-settings-state.js';
|
|
3
|
+
|
|
4
|
+
function render(state) {
|
|
5
|
+
var section = document.getElementById('passkey-policy');
|
|
6
|
+
if (!section) return;
|
|
7
|
+
section.hidden = !state.available;
|
|
8
|
+
var input = section.querySelector('[name=origin]');
|
|
9
|
+
if (state.draft === undefined) input.value = state.origin || '';
|
|
10
|
+
input.disabled = !!state.pending || !!state.locked;
|
|
11
|
+
section.querySelector('[name=currentPassword]').disabled = !!state.pending;
|
|
12
|
+
section.querySelector('button').disabled = !!state.pending || !!state.locked;
|
|
13
|
+
section.querySelector('[data-passkey-editor]').hidden = !!state.locked;
|
|
14
|
+
section.querySelector('[role=status]').textContent = state.error || (state.pending ? 'Saving…' : state.origin ?
|
|
15
|
+
'Passkeys are available at ' + state.origin + (state.locked ? '. Remove registered passkeys before changing this address.' : '.') : 'Not configured. Password sign-in remains available.');
|
|
16
|
+
}
|
|
17
|
+
export async function loadPasskeyServerSettings() {
|
|
18
|
+
if (!document.getElementById('passkey-policy') || (store.get('passkeyPolicy') || {}).draft !== undefined) return;
|
|
19
|
+
try {
|
|
20
|
+
var response = await fetch('/api/passkey-policy'); var data = await response.json();
|
|
21
|
+
if (!response.ok) throw new Error(data.error || 'Could not load passkey settings.');
|
|
22
|
+
store.set({ passkeyPolicy: data }); render(data);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
var state = Object.assign({}, store.get('passkeyPolicy'), { error: error.message });
|
|
25
|
+
store.set({ passkeyPolicy: state }); render(state);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function initPasskeyServerSettings() {
|
|
29
|
+
var anchor = document.getElementById('account-auth-policy');
|
|
30
|
+
if (!anchor || document.getElementById('passkey-policy')) return;
|
|
31
|
+
var section = document.createElement('section'); section.id = 'passkey-policy'; section.className = 'settings-card'; section.hidden = true;
|
|
32
|
+
section.innerHTML = '<h3>Passkey sign-in address</h3><p class="settings-hint">Configure the shared server address for passkey sign-in. Users register and manage their own passkeys in Personal settings → Account. Passwords remain available.</p>' +
|
|
33
|
+
'<form><label class="settings-label" for="passkey-origin">Sign-in address</label><input class="us-text-input" id="passkey-origin" name="origin" type="url" placeholder="https://clay.example.com" required>' +
|
|
34
|
+
'<p class="settings-hint">Use the permanent HTTPS address for this server. Local development can use http://localhost with its port.</p>' +
|
|
35
|
+
'<div data-passkey-editor><label class="settings-label" for="passkey-admin-password">Administrator password</label><input class="us-text-input" id="passkey-admin-password" name="currentPassword" type="password" autocomplete="current-password" required>' +
|
|
36
|
+
'<div class="settings-form-actions"><button class="settings-btn-sm" type="submit">Save passkey address</button></div></div></form><p class="settings-hint" role="status"></p>';
|
|
37
|
+
anchor.after(section);
|
|
38
|
+
var input = section.querySelector('[name=origin]');
|
|
39
|
+
input.oninput = function () {
|
|
40
|
+
var state = Object.assign({}, store.get('passkeyPolicy'), { draft: input.value, error: '' });
|
|
41
|
+
if (input.value === (state.origin || '')) { delete state.draft; clearSettingsDraft('passkeys'); }
|
|
42
|
+
else setSettingsDraft('passkeys', true);
|
|
43
|
+
store.set({ passkeyPolicy: state }); render(state);
|
|
44
|
+
};
|
|
45
|
+
section.querySelector('form').onsubmit = async function (event) {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
var state = Object.assign({}, store.get('passkeyPolicy'), { pending: true, error: '' });
|
|
48
|
+
store.set({ passkeyPolicy: state }); render(state);
|
|
49
|
+
var password = section.querySelector('[name=currentPassword]');
|
|
50
|
+
try {
|
|
51
|
+
var response = await fetch('/api/passkey-policy', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ origin: input.value, currentPassword: password.value }) });
|
|
52
|
+
var data = await response.json(); if (!response.ok) throw new Error(data.error || 'Could not save passkey settings.');
|
|
53
|
+
state = data; clearSettingsDraft('passkeys');
|
|
54
|
+
} catch (error) { state.pending = false; state.error = error.message; }
|
|
55
|
+
finally { password.value = ''; store.set({ passkeyPolicy: state }); render(state); }
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -366,11 +366,12 @@ export function renderDetail(detailEl, entry, handlers) {
|
|
|
366
366
|
formatTime(entry.updatedAt || entry.createdAt),
|
|
367
367
|
].filter(Boolean).join(" · ");
|
|
368
368
|
setText(header, ".project-log-byline", byline);
|
|
369
|
-
if (handlers && handlers.canDelete && !entry.scheduledResult) {
|
|
369
|
+
if (handlers && handlers.canDelete && !entry.scheduledResult && !entry.deleted) {
|
|
370
370
|
var deleteButton = document.createElement("button");
|
|
371
371
|
deleteButton.type = "button";
|
|
372
372
|
deleteButton.className = "project-log-delete-button";
|
|
373
|
-
deleteButton.innerHTML = '<i data-lucide="trash-2"></i><span
|
|
373
|
+
deleteButton.innerHTML = '<i data-lucide="trash-2"></i><span></span>';
|
|
374
|
+
deleteButton.querySelector("span").textContent = handlers.deleteLabel || "Delete log";
|
|
374
375
|
deleteButton.addEventListener("click", function () { handlers.onDelete(entry); });
|
|
375
376
|
header.appendChild(deleteButton);
|
|
376
377
|
}
|
|
@@ -10,6 +10,7 @@ function status(id, text) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function sendSetting(type, values, buttonId, statusId) {
|
|
13
|
+
if (type === 'set_pin' && store.get('serverPinAvailable') !== true) return;
|
|
13
14
|
var ws = getWs();
|
|
14
15
|
if (!ws || ws.readyState !== 1) { status(statusId, 'Disconnected. Your change has not been saved.'); return; }
|
|
15
16
|
var button = document.getElementById(buttonId);
|
|
@@ -29,6 +30,15 @@ function sendSetting(type, values, buttonId, statusId) {
|
|
|
29
30
|
}, 12000);
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
export function setServerPinAvailability(available) {
|
|
34
|
+
store.set({ serverPinAvailable: !!available });
|
|
35
|
+
var section = document.getElementById('settings-server-pin');
|
|
36
|
+
if (!section) return;
|
|
37
|
+
section.hidden = !available;
|
|
38
|
+
section.querySelectorAll('input,button').forEach(function (element) { element.disabled = !available; });
|
|
39
|
+
if (!available) hidePinForm();
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
export function handleServerSettingResult(msg) {
|
|
33
43
|
var type = msg.type.replace(/_result$/, '');
|
|
34
44
|
var pending = Object.assign({}, store.get('serverSettingsPending') || {});
|