clay-server 5.1.0 → 5.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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-server-settings.js +1 -0
- package/lib/project-user-knowledge.js +2 -1
- package/lib/public/auth/account-login.css +98 -0
- package/lib/public/auth/account-login.js +104 -0
- package/lib/public/css/mate-conversation.css +2 -2
- 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-messages.js +4 -0
- 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-settings.js +2 -1
- package/lib/server.js +2 -0
- 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/package.json +2 -1
|
@@ -1483,6 +1483,10 @@ export function processMessage(msg) {
|
|
|
1483
1483
|
addContextOverflowMessage(msg);
|
|
1484
1484
|
break;
|
|
1485
1485
|
|
|
1486
|
+
case "account_auth_required":
|
|
1487
|
+
window.location.reload();
|
|
1488
|
+
break;
|
|
1489
|
+
|
|
1486
1490
|
case "auth_required":
|
|
1487
1491
|
removeMatePreThinking();
|
|
1488
1492
|
setActivity(null);
|
|
@@ -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') || {});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { initStorageUsage, loadStorageUsage } from './server-storage-settings.js';
|
|
2
|
+
import { initAccountAuthSettings, loadAccountAuthSettings } from './account-auth-settings.js';
|
|
2
3
|
// Full-app server administration shell. Page state survives navigation and closing.
|
|
3
4
|
import { store } from './store.js';
|
|
4
5
|
import { getWs } from './ws-ref.js';
|
|
@@ -7,7 +8,7 @@ import { closeFileViewer } from './filebrowser.js';
|
|
|
7
8
|
import { checkAdminAccess, loadAdminSection } from './admin.js';
|
|
8
9
|
import { openUserSettings } from './user-settings.js';
|
|
9
10
|
import { initNetworkSettings, loadNetworkSettings } from './server-network-settings.js';
|
|
10
|
-
import { initServerControls } from './server-settings-controls.js';
|
|
11
|
+
import { initServerControls, setServerPinAvailability } from './server-settings-controls.js';
|
|
11
12
|
import { initServerEnvironment, loadSharedEnv } from './server-settings-environment.js';
|
|
12
13
|
import { renderSettingsNavigation } from './server-settings-navigation.js';
|
|
13
14
|
import { hasSettingsDrafts, confirmSettingsAction, trapSettingsFocus } from './server-settings-state.js';
|
|
@@ -41,6 +42,7 @@ function selectPage(id, focus) {
|
|
|
41
42
|
});
|
|
42
43
|
document.getElementById('settings-nav-dropdown').value = id;
|
|
43
44
|
if (id === 'network') loadNetworkSettings();
|
|
45
|
+
if (id === 'security') loadAccountAuthSettings();
|
|
44
46
|
if (id === 'storage') loadStorageUsage();
|
|
45
47
|
if (id === 'environment') loadSharedEnv();
|
|
46
48
|
if (id.indexOf('admin-') === 0) {
|
|
@@ -64,6 +66,8 @@ async function openSettings() {
|
|
|
64
66
|
var status = document.getElementById('settings-access-status');
|
|
65
67
|
status.textContent = 'Checking administrator access…';
|
|
66
68
|
store.set({ serverSettingsAuthorized: false });
|
|
69
|
+
var serverPinSection = document.getElementById('settings-server-pin');
|
|
70
|
+
if (serverPinSection) serverPinSection.hidden = true;
|
|
67
71
|
root().querySelectorAll('.server-settings-section').forEach(function (page) { page.classList.remove('active'); });
|
|
68
72
|
try {
|
|
69
73
|
var response = await fetch('/api/me', { credentials: 'same-origin' });
|
|
@@ -78,6 +82,7 @@ async function openSettings() {
|
|
|
78
82
|
await checkAdminAccess();
|
|
79
83
|
if (!isSettingsOpen()) return;
|
|
80
84
|
store.set({ serverSettingsAuthorized: true });
|
|
85
|
+
setServerPinAvailability(!data.multiUser);
|
|
81
86
|
status.textContent = '';
|
|
82
87
|
renderSettingsNavigation(!!data.multiUser, selectPage);
|
|
83
88
|
selectPage(store.get('serverSettingsPage') || 'overview', false);
|
|
@@ -111,7 +116,7 @@ function closeSettings() {
|
|
|
111
116
|
|
|
112
117
|
export function initServerSettings() {
|
|
113
118
|
if (!root()) return;
|
|
114
|
-
initNetworkSettings(); initServerControls(); initServerEnvironment(); initStorageUsage();
|
|
119
|
+
initNetworkSettings(); initServerControls(); initServerEnvironment(); initStorageUsage(); initAccountAuthSettings();
|
|
115
120
|
document.getElementById('server-settings-btn').addEventListener('click', openSettings);
|
|
116
121
|
document.getElementById('server-settings-close').onclick = closeSettings;
|
|
117
122
|
document.getElementById('settings-personal-link').onclick = function () {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { refreshIcons } from './icons.js';
|
|
2
|
+
import { resetAccountSessions } from './user-settings-sessions.js';
|
|
2
3
|
import { confirmSettingsAction as showConfirmDialog } from './server-settings-state.js';
|
|
3
4
|
import { store } from './store.js';
|
|
4
5
|
import { getWs } from './ws-ref.js';
|
|
@@ -10,6 +11,7 @@ import { syncCapsulePreference } from './capsule-preference.js';
|
|
|
10
11
|
import { getEmailAccountListCache } from './context-sources.js';
|
|
11
12
|
function applyProjectMateDmsClass(enabled) { document.body.classList.toggle('project-mate-dms-disabled', !enabled); }
|
|
12
13
|
export function showPinForm() {
|
|
14
|
+
if (store.get('userPasswordEnabled')) { window.location.href = '/account/password'; return; }
|
|
13
15
|
var pinForm = document.getElementById('us-pin-form');
|
|
14
16
|
if (!pinForm) return;
|
|
15
17
|
if(!pinForm.classList.contains('hidden'))return;
|
|
@@ -76,6 +78,7 @@ export function hidePinForm(force) {
|
|
|
76
78
|
// --- Account population ---
|
|
77
79
|
|
|
78
80
|
export function populateAccount() {
|
|
81
|
+
resetAccountSessions();
|
|
79
82
|
fetch('/api/profile').then(function (r) {
|
|
80
83
|
if (!r.ok) return null;
|
|
81
84
|
return r.json();
|
|
@@ -98,9 +101,19 @@ export function populateAccount() {
|
|
|
98
101
|
if (data.role) parts.push(data.role.charAt(0).toUpperCase() + data.role.slice(1));
|
|
99
102
|
accountSubline.textContent = parts.length ? parts.join(' · ') : 'Local profile';
|
|
100
103
|
}
|
|
101
|
-
store.set({userPinEnabled:!!data.pinEnabled});
|
|
104
|
+
store.set({userPinEnabled:!!data.pinEnabled, userPasswordEnabled:!!data.passwordEnabled});
|
|
105
|
+
var passkeyRow = document.getElementById('us-passkey-row');
|
|
106
|
+
if (passkeyRow) passkeyRow.hidden = !data.passwordEnabled;
|
|
107
|
+
var sessionsRow = document.getElementById('us-sessions-row');
|
|
108
|
+
if (sessionsRow) sessionsRow.hidden = !data.passwordEnabled && !data.pinEnabled;
|
|
102
109
|
if (pinSetBtnEl) {
|
|
103
110
|
pinSetBtnEl.textContent = store.get('userPinEnabled') ? 'Change PIN' : 'Set PIN';
|
|
111
|
+
if (data.passwordEnabled) pinSetBtnEl.textContent = 'Change password';
|
|
112
|
+
var row = pinSetBtnEl.closest('.settings-field-row');
|
|
113
|
+
if (row) {
|
|
114
|
+
row.querySelector('.settings-label').textContent = data.passwordEnabled ? 'Password' : 'Account PIN';
|
|
115
|
+
row.querySelector('.settings-hint').textContent = data.passwordEnabled ? 'Change your account password or passphrase.' : 'Manage your account PIN.';
|
|
116
|
+
}
|
|
104
117
|
}
|
|
105
118
|
if (pinFormTitle) {
|
|
106
119
|
pinFormTitle.textContent = store.get('userPinEnabled') ? 'Change PIN' : 'Set PIN';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
|
|
3
|
+
function describe(value) {
|
|
4
|
+
if (!value || value === 'Earlier session') return 'Unknown device';
|
|
5
|
+
var browser = /Edg(?:e|A|iOS)?\//.test(value) ? 'Edge' : /Firefox\/|FxiOS\//.test(value) ? 'Firefox' : /Chrome\/|CriOS\//.test(value) ? 'Chrome' : /Safari\//.test(value) ? 'Safari' : 'Browser';
|
|
6
|
+
var system = /Android/.test(value) ? 'Android' : /iPhone|iPad/.test(value) ? 'iOS' : /Windows/.test(value) ? 'Windows' : /Macintosh/.test(value) ? 'macOS' : /Linux/.test(value) ? 'Linux' : '';
|
|
7
|
+
return browser + (system ? ' on ' + system : '');
|
|
8
|
+
}
|
|
9
|
+
function state() { return store.get('accountSessionsPanel') || { epoch: 0, open: false, entries: [] }; }
|
|
10
|
+
function update(values) { store.set({ accountSessionsPanel: Object.assign({}, state(), values) }); render(); }
|
|
11
|
+
function render() {
|
|
12
|
+
var panel = document.getElementById('us-sessions-panel'); if (!panel) return;
|
|
13
|
+
var scroll = []; var ancestor = panel.parentElement;
|
|
14
|
+
while (ancestor) { scroll.push({ element: ancestor, top: ancestor.scrollTop }); ancestor = ancestor.parentElement; }
|
|
15
|
+
var current = state(); var toggle = document.getElementById('us-sessions-toggle');
|
|
16
|
+
panel.hidden = !current.open; toggle.setAttribute('aria-expanded', String(!!current.open));
|
|
17
|
+
toggle.textContent = current.open ? 'Hide sessions' : 'Manage sessions'; toggle.disabled = !!current.saving;
|
|
18
|
+
var list = panel.querySelector('[data-list]'); list.replaceChildren();
|
|
19
|
+
var previousGroup = panel.querySelector('[data-earlier]'); if (previousGroup) previousGroup.remove();
|
|
20
|
+
var unknown = current.entries.filter(function (entry) { return entry.unknownDevice && !entry.current; });
|
|
21
|
+
var older = document.createElement('details'); older.dataset.earlier = ''; var summary = document.createElement('summary');
|
|
22
|
+
summary.className = 'settings-label'; summary.textContent = 'Earlier sessions without device details (' + unknown.length + ')'; older.append(summary);
|
|
23
|
+
var explanation = document.createElement('p'); explanation.className = 'settings-hint';
|
|
24
|
+
explanation.textContent = 'These sessions predate device tracking. Their browser and previous activity are unknown; matching labels do not mean they are the same device.';
|
|
25
|
+
older.append(explanation); older.open = !!current.showEarlier;
|
|
26
|
+
older.ontoggle = function () { if (older.isConnected) store.set({ accountSessionsPanel: Object.assign({}, state(), { showEarlier: older.open }) }); };
|
|
27
|
+
var olderList = document.createElement('div'); olderList.setAttribute('role', 'list'); older.append(olderList);
|
|
28
|
+
current.entries.forEach(function (entry) {
|
|
29
|
+
var row = document.createElement('div'); row.className = 'settings-field-row'; row.setAttribute('role', 'listitem');
|
|
30
|
+
var detail = document.createElement('div'); var title = document.createElement('span'); title.className = 'settings-label';
|
|
31
|
+
title.textContent = describe(entry.userAgent) + (entry.current ? ' · This session' : '');
|
|
32
|
+
var hint = document.createElement('p'); hint.className = 'settings-hint';
|
|
33
|
+
hint.textContent = entry.method + (entry.lastSeenAt ? ' · Last active ' + new Date(entry.lastSeenAt).toLocaleString() : ' · Previous activity unknown') +
|
|
34
|
+
(entry.address ? ' · Last observed IP ' + entry.address : '') +
|
|
35
|
+
(entry.expiresAt ? ' · Expires ' + new Date(entry.expiresAt).toLocaleDateString() : ' · Compatibility session');
|
|
36
|
+
detail.append(title, hint);
|
|
37
|
+
var button = document.createElement('button'); button.type = 'button'; button.className = 'settings-btn-sm'; button.textContent = 'Sign out';
|
|
38
|
+
button.dataset.sessionId = entry.id; button.setAttribute('aria-label', 'Sign out ' + describe(entry.userAgent) + (entry.current ? ' current session' : ''));
|
|
39
|
+
button.disabled = !!current.pending;
|
|
40
|
+
button.onclick = function () { choose(entry.id); };
|
|
41
|
+
if (current.target === entry.id) {
|
|
42
|
+
var confirmation = document.createElement('div'); confirmation.className = 'us-session-row-confirm';
|
|
43
|
+
confirmation.setAttribute('role', 'group'); confirmation.setAttribute('aria-label', 'Confirm session sign-out');
|
|
44
|
+
var question = document.createElement('p'); question.className = 'settings-hint';
|
|
45
|
+
question.textContent = entry.current ? 'Sign out this browser? You will need to sign in again.' : 'Sign out this session?';
|
|
46
|
+
var actions = document.createElement('div'); actions.className = 'settings-form-actions';
|
|
47
|
+
var confirm = document.createElement('button'); confirm.type = 'button'; confirm.className = 'settings-btn-sm settings-btn-danger';
|
|
48
|
+
confirm.dataset.rowConfirm = ''; confirm.textContent = current.saving ? 'Signing out…' : 'Sign out'; confirm.disabled = !!current.pending;
|
|
49
|
+
confirm.onclick = function () { request(entry.id); };
|
|
50
|
+
var cancel = document.createElement('button'); cancel.type = 'button'; cancel.className = 'settings-btn-sm';
|
|
51
|
+
cancel.dataset.rowCancel = ''; cancel.textContent = 'Cancel'; cancel.disabled = !!current.pending;
|
|
52
|
+
cancel.onclick = function () { update({ target: null, error: '' }); restoreFocus(entry.id); };
|
|
53
|
+
actions.append(confirm, cancel); confirmation.append(question, actions);
|
|
54
|
+
if (current.error) { var error = document.createElement('p'); error.className = 'settings-status'; error.setAttribute('role', 'alert'); error.textContent = current.error; confirmation.append(error); }
|
|
55
|
+
row.append(detail, confirmation);
|
|
56
|
+
} else row.append(detail, button);
|
|
57
|
+
(entry.unknownDevice && !entry.current ? olderList : list).append(row);
|
|
58
|
+
});
|
|
59
|
+
if (unknown.length) list.after(older);
|
|
60
|
+
panel.querySelector('[data-status]').textContent = current.target && current.target !== 'others' ? '' : current.error || (current.pending ? current.saving ? 'Signing out…' : 'Loading sessions…' : current.message || '');
|
|
61
|
+
panel.querySelector('[data-retry]').hidden = !current.error || !!current.target;
|
|
62
|
+
panel.querySelector('[data-others]').disabled = !!current.pending || !current.entries.some(function (entry) { return !entry.current; });
|
|
63
|
+
panel.querySelector('[data-confirmation]').hidden = current.target !== 'others';
|
|
64
|
+
var selected = current.entries.find(function (entry) { return entry.id === current.target; });
|
|
65
|
+
panel.querySelector('[data-question]').textContent = selected && selected.current ? 'Sign out this browser? Your settings will close and you will need to sign in again.' : 'Sign out the selected sessions? Their open connections will close.';
|
|
66
|
+
panel.querySelector('[data-confirm]').disabled = !!current.pending;
|
|
67
|
+
panel.querySelector('[data-cancel]').disabled = !!current.pending;
|
|
68
|
+
scroll.forEach(function (position) { position.element.scrollTop = position.top; });
|
|
69
|
+
}
|
|
70
|
+
function choose(id) {
|
|
71
|
+
update({ target: id, error: '', message: '' });
|
|
72
|
+
document.querySelector(id === 'others' ? '#us-sessions-panel [data-confirm]' : '#us-sessions-panel [data-row-confirm]').focus({ preventScroll: true });
|
|
73
|
+
}
|
|
74
|
+
function restoreFocus(id) {
|
|
75
|
+
var panel = document.getElementById('us-sessions-panel');
|
|
76
|
+
var button = Array.from(panel.querySelectorAll('[data-session-id]')).find(function (item) { return item.dataset.sessionId === id; });
|
|
77
|
+
(button || (id === 'others' ? panel.querySelector('[data-others]') : document.getElementById('us-sessions-toggle'))).focus({ preventScroll: true });
|
|
78
|
+
}
|
|
79
|
+
async function request(id) {
|
|
80
|
+
var entries = state().entries; var index = entries.findIndex(function (entry) { return entry.id === id; });
|
|
81
|
+
var neighbor = entries[index + 1] || entries[index - 1];
|
|
82
|
+
var epoch = state().epoch + 1;
|
|
83
|
+
update({ epoch: epoch, pending: true, saving: !!id, error: '', message: '' });
|
|
84
|
+
try {
|
|
85
|
+
var response = await fetch('/api/account-sessions', id ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id }) } : {});
|
|
86
|
+
var data = await response.json();
|
|
87
|
+
if (epoch !== state().epoch) return;
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
if (response.status === 401 || response.status === 403) update({ entries: [], target: null });
|
|
90
|
+
throw new Error(data.error || 'Could not manage sessions.');
|
|
91
|
+
}
|
|
92
|
+
if (data.signedOut) { window.location.href = '/'; return; }
|
|
93
|
+
update({ entries: data.sessions, target: null, pending: false, saving: false, message: id ? 'Selected sessions signed out.' : '' });
|
|
94
|
+
if (id) restoreFocus(id === 'others' ? id : neighbor && neighbor.id);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (epoch === state().epoch) update({ pending: false, saving: false, error: error.message });
|
|
97
|
+
var retry = document.querySelector('#us-sessions-panel [data-row-confirm]');
|
|
98
|
+
if (epoch === state().epoch && id && retry) retry.focus({ preventScroll: true });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function resetAccountSessions() {
|
|
102
|
+
update({ epoch: state().epoch + 1, open: false, entries: [], target: null, pending: false, saving: false, error: '', message: '', showEarlier: false });
|
|
103
|
+
}
|
|
104
|
+
export function initAccountSessions() {
|
|
105
|
+
var toggle = document.getElementById('us-sessions-toggle'); if (!toggle || document.getElementById('us-sessions-panel')) return;
|
|
106
|
+
var panel = document.createElement('div'); panel.id = 'us-sessions-panel'; panel.hidden = true;
|
|
107
|
+
panel.innerHTML = '<p class="settings-hint">Each entry represents a browser session. Account sessions expire after 30 days, or after 7 days without use.</p>' +
|
|
108
|
+
'<div data-list role="list" aria-label="Signed-in sessions"></div><div class="settings-form-actions"><button type="button" class="settings-btn-sm" data-others>Sign out other sessions</button></div>' +
|
|
109
|
+
'<div data-confirmation role="group" aria-label="Confirm sign-out" hidden><p class="settings-hint" data-question></p><div class="settings-form-actions"><button type="button" class="settings-btn-sm settings-btn-danger" data-confirm>Sign out</button><button type="button" class="settings-btn-sm" data-cancel>Cancel</button></div></div>' +
|
|
110
|
+
'<p class="settings-status" role="status" data-status></p><button type="button" class="settings-btn-sm" data-retry hidden>Retry</button>';
|
|
111
|
+
toggle.closest('.settings-field-row').after(panel);
|
|
112
|
+
toggle.onclick = function () {
|
|
113
|
+
if (state().open) { resetAccountSessions(); toggle.focus(); }
|
|
114
|
+
else { update({ open: true }); request(); }
|
|
115
|
+
};
|
|
116
|
+
panel.querySelector('[data-others]').onclick = function () { choose('others'); };
|
|
117
|
+
panel.querySelector('[data-retry]').onclick = function () { request(); };
|
|
118
|
+
panel.querySelector('[data-confirm]').onclick = function () { request(state().target); };
|
|
119
|
+
panel.querySelector('[data-cancel]').onclick = function () { var id = state().target; update({ target: null, error: '' }); restoreFocus(id); };
|
|
120
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { initPersonalAppearance } from './user-settings-appearance.js';
|
|
2
|
+
import { initAccountSessions, resetAccountSessions } from './user-settings-sessions.js';
|
|
2
3
|
import { initPersonalVoice } from './user-settings-voice.js';
|
|
3
4
|
import { isolateSettingsDialog } from './server-settings-state.js';
|
|
4
5
|
import { initPersonalProfile } from './user-settings-profile.js';
|
|
@@ -29,6 +30,7 @@ var pinCancelBtn = null;
|
|
|
29
30
|
|
|
30
31
|
export function initUserSettings() {
|
|
31
32
|
buildUserSettingsLayout();
|
|
33
|
+
initAccountSessions();
|
|
32
34
|
settingsEl = document.getElementById('user-settings');
|
|
33
35
|
openBtn = document.getElementById('user-settings-btn');
|
|
34
36
|
closeBtn = document.getElementById('user-settings-close');
|
|
@@ -204,6 +206,7 @@ export function openUserSettings(initialSection) {
|
|
|
204
206
|
|
|
205
207
|
export function closeUserSettings() {
|
|
206
208
|
guardUserSettingsLeave(function () {
|
|
209
|
+
resetAccountSessions();
|
|
207
210
|
finishUserSettingsExtras();
|
|
208
211
|
settingsEl.classList.add('hidden');
|
|
209
212
|
var restore=store.get('userSettingsRestore');if(restore)restore();store.set({userSettingsRestore:null});
|
|
@@ -7,6 +7,7 @@ function state() { return store.get('youActivity') || {}; }
|
|
|
7
7
|
function put(values) { store.set({ youActivity: Object.assign({}, state(), values) }); }
|
|
8
8
|
function el(id) { return document.getElementById(id); }
|
|
9
9
|
function describe(item) {
|
|
10
|
+
if (item.action === 'delete') return 'You deleted a memory from Me.';
|
|
10
11
|
if (item.action === 'archive') return 'Clay stopped using a memory about you.';
|
|
11
12
|
return (item.action === 'update' ? 'Clay updated: ' : 'Clay remembered: ') + (item.summary || item.title);
|
|
12
13
|
}
|
|
@@ -67,7 +68,7 @@ function render() {
|
|
|
67
68
|
var text = document.createElement('span'); text.textContent = describe(item);
|
|
68
69
|
var time = document.createElement('small'); time.textContent = relativeTime(item.at);
|
|
69
70
|
button.append(text, time);
|
|
70
|
-
button.addEventListener('click', function () { close(); openYou(item.action === 'archive' ? null : item.ref); });
|
|
71
|
+
button.addEventListener('click', function () { close(); openYou(item.action === 'archive' || item.action === 'delete' ? null : item.ref); });
|
|
71
72
|
list.appendChild(button);
|
|
72
73
|
});
|
|
73
74
|
el('you-activity-more').classList.toggle('hidden', state().nextOffset == null);
|
|
@@ -76,6 +77,7 @@ export function handleYouActivity(msg) {
|
|
|
76
77
|
if (!el('you-activity') || (msg.activity && msg.activity.seen)) return;
|
|
77
78
|
if (msg.type === 'you_reviewed') { put({ error: '' }); render(); return; }
|
|
78
79
|
if (msg.type === 'you_changed') {
|
|
80
|
+
if (msg.activity && msg.activity.deleted) { request(); return; }
|
|
79
81
|
if (msg.activity && msg.activity.error) { put({ error: msg.activity.error }); announce(msg.activity.error); return; }
|
|
80
82
|
put({ error: '' });
|
|
81
83
|
if (msg.activity && msg.activity.id && (state().seen || []).indexOf(msg.activity.id) === -1) {
|
|
@@ -5,6 +5,8 @@ import { refreshIcons } from './icons.js';
|
|
|
5
5
|
import { renderList, renderDetail, categoryLabel } from './project-logs-render.js';
|
|
6
6
|
import { registerRightWorkbench, claimRightWorkbench, releaseRightWorkbench } from './right-workbench.js';
|
|
7
7
|
|
|
8
|
+
var deleteDialog = null;
|
|
9
|
+
var deleteDialogFocus = null;
|
|
8
10
|
function state() { return store.get('you') || {}; }
|
|
9
11
|
function put(values) { store.set({ you: Object.assign({}, state(), values) }); }
|
|
10
12
|
function element(id) { return document.getElementById(id); }
|
|
@@ -70,6 +72,37 @@ function requestEntry(ref) {
|
|
|
70
72
|
put({ readId: sendYou('you_read', { ref: ref }), selected: ref, scroll: element('you-list').scrollTop });
|
|
71
73
|
status(state().readId ? 'Loading…' : 'Reconnect to read this memory.');
|
|
72
74
|
}
|
|
75
|
+
function closeDeleteDialog(force) {
|
|
76
|
+
if (!deleteDialog) return;
|
|
77
|
+
if (!force && state().deleteId) return;
|
|
78
|
+
deleteDialog.classList.add('hidden');
|
|
79
|
+
put({ deleteId: null, deleteEntry: null });
|
|
80
|
+
if (deleteDialogFocus && deleteDialogFocus.isConnected) deleteDialogFocus.focus();
|
|
81
|
+
deleteDialogFocus = null;
|
|
82
|
+
}
|
|
83
|
+
function sendDelete() {
|
|
84
|
+
var entry = state().deleteEntry;
|
|
85
|
+
if (!entry || state().deleteId) return;
|
|
86
|
+
var requestId = sendYou('you_delete', { ref: entry.ref, expectedRevision: entry.revisions });
|
|
87
|
+
put({ deleteId: requestId });
|
|
88
|
+
var confirm = deleteDialog.querySelector('[data-action="delete"]');
|
|
89
|
+
var cancel = deleteDialog.querySelector('[data-action="cancel"]');
|
|
90
|
+
confirm.disabled = true; cancel.disabled = true;
|
|
91
|
+
deleteDialog.querySelector('.you-delete-dialog-status').textContent = requestId ? 'Deleting memory… Stored audit history remains.' : 'Reconnect to delete this memory.';
|
|
92
|
+
if (requestId) { deleteDialog.tabIndex = -1; deleteDialog.focus(); }
|
|
93
|
+
if (!requestId) { put({ deleteId: null }); confirm.disabled = false; cancel.disabled = false; }
|
|
94
|
+
}
|
|
95
|
+
function deleteEntry(entry) {
|
|
96
|
+
if (!entry || state().deleteId || !deleteDialog) return;
|
|
97
|
+
put({ deleteEntry: entry });
|
|
98
|
+
deleteDialogFocus = document.activeElement;
|
|
99
|
+
deleteDialog.querySelector('.you-delete-dialog-title').textContent = entry.title || 'Untitled memory';
|
|
100
|
+
deleteDialog.querySelector('.you-delete-dialog-status').textContent = '';
|
|
101
|
+
deleteDialog.querySelector('[data-action="delete"]').disabled = false;
|
|
102
|
+
deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
|
|
103
|
+
deleteDialog.classList.remove('hidden');
|
|
104
|
+
deleteDialog.querySelector('[data-action="cancel"]').focus();
|
|
105
|
+
}
|
|
73
106
|
function windowState(wide, fullscreen) {
|
|
74
107
|
put({ wide: wide, fullscreen: fullscreen });
|
|
75
108
|
element('you-panel').classList.toggle('project-logs-wide', wide && !fullscreen);
|
|
@@ -117,6 +150,25 @@ function ensurePanel() {
|
|
|
117
150
|
'<div id="you-list" class="project-logs-list" role="list"></div><main id="you-detail" class="project-logs-detail hidden"></main>' +
|
|
118
151
|
'<button id="you-more" type="button" class="you-more hidden">Load more</button>';
|
|
119
152
|
document.getElementById('main-panels').appendChild(panel);
|
|
153
|
+
var dialog = document.createElement('div');
|
|
154
|
+
dialog.className = 'project-log-delete-dialog you-delete-dialog hidden';
|
|
155
|
+
dialog.setAttribute('role', 'dialog'); dialog.setAttribute('aria-modal', 'true'); dialog.setAttribute('aria-labelledby', 'you-delete-heading'); dialog.tabIndex = -1;
|
|
156
|
+
dialog.innerHTML = '<div class="project-log-delete-dialog-card"><h2 id="you-delete-heading">Delete this memory?</h2><p>This removes the memory from Me immediately. Clay’s audit history will retain that it was deleted.</p><p class="project-log-delete-dialog-title you-delete-dialog-title"></p><p class="project-log-delete-dialog-status you-delete-dialog-status" role="status"></p><div class="project-log-delete-dialog-actions"><button type="button" data-action="cancel">Cancel</button><button type="button" class="danger" data-action="delete">Delete memory</button></div></div>';
|
|
157
|
+
panel.appendChild(dialog); deleteDialog = dialog;
|
|
158
|
+
dialog.querySelector('[data-action="cancel"]').addEventListener('click', function () { closeDeleteDialog(false); });
|
|
159
|
+
dialog.querySelector('[data-action="delete"]').addEventListener('click', sendDelete);
|
|
160
|
+
dialog.addEventListener('keydown', function (event) {
|
|
161
|
+
event.stopPropagation();
|
|
162
|
+
if (event.key === 'Escape') { event.preventDefault(); closeDeleteDialog(false); return; }
|
|
163
|
+
if (event.key === 'Tab') {
|
|
164
|
+
var buttons = Array.from(dialog.querySelectorAll('button:not(:disabled)'));
|
|
165
|
+
if (!buttons.length) { event.preventDefault(); dialog.focus(); return; }
|
|
166
|
+
var first = buttons[0]; var last = buttons[buttons.length - 1];
|
|
167
|
+
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
|
168
|
+
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
dialog.addEventListener('click', function (event) { if (event.target === dialog) closeDeleteDialog(false); });
|
|
120
172
|
element('you-close').addEventListener('click', closeYou);
|
|
121
173
|
element('you-back').addEventListener('click', showList);
|
|
122
174
|
element('you-wide').addEventListener('click', function () { windowState(!state().wide, false); });
|
|
@@ -159,6 +211,7 @@ export function openYou(ref) {
|
|
|
159
211
|
if (ref) element('you-close').focus(); else element('you-search').focus();
|
|
160
212
|
}
|
|
161
213
|
export function closeYou() {
|
|
214
|
+
closeDeleteDialog(true);
|
|
162
215
|
releaseRightWorkbench('you');
|
|
163
216
|
if (!state().open) return;
|
|
164
217
|
element('you-panel').classList.add('hidden');
|
|
@@ -172,6 +225,17 @@ export function closeYou() {
|
|
|
172
225
|
export function handleYouMessage(msg) {
|
|
173
226
|
if (msg.type === 'you_reviewed') { if (state().open) { requestList(); if (state().selected) requestEntry(state().selected); } return; }
|
|
174
227
|
if (msg.type === 'you_changed') {
|
|
228
|
+
if (msg.activity && msg.activity.deleted) {
|
|
229
|
+
requestAttention();
|
|
230
|
+
if (state().selected === msg.activity.ref) {
|
|
231
|
+
if (state().deleteEntry && state().deleteEntry.ref === msg.activity.ref) closeDeleteDialog(true);
|
|
232
|
+
put({ readId: null, renderedRef: null, renderedRevision: null });
|
|
233
|
+
showList();
|
|
234
|
+
element('you-search').focus();
|
|
235
|
+
status('This memory was deleted and is no longer available.');
|
|
236
|
+
} else if (state().open) requestList();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
175
239
|
requestAttention();
|
|
176
240
|
if (msg.activity && msg.activity.seen) { if (state().open && !state().selected) requestList(); return; }
|
|
177
241
|
if (msg.activity && msg.activity.error) { status(msg.activity.error); return; }
|
|
@@ -179,7 +243,7 @@ export function handleYouMessage(msg) {
|
|
|
179
243
|
return;
|
|
180
244
|
}
|
|
181
245
|
var s = state();
|
|
182
|
-
var key = { you_attention: 'attentionId', you_seen: 'seenId', you_list: 'listId', you_read: 'readId', you_comment: 'commentId', you_retry: 'retryId' }[msg.operation];
|
|
246
|
+
var key = { you_attention: 'attentionId', you_seen: 'seenId', you_list: 'listId', you_read: 'readId', you_comment: 'commentId', you_retry: 'retryId', you_delete: 'deleteId' }[msg.operation];
|
|
183
247
|
if (!key || !s[key] || s[key] !== msg.requestId) return;
|
|
184
248
|
var cleared = {}; cleared[key] = null; put(cleared);
|
|
185
249
|
if (msg.operation === 'you_comment' && element('you-comment-input')) element('you-comment-input').form.querySelector('button').disabled = false;
|
|
@@ -189,10 +253,25 @@ export function handleYouMessage(msg) {
|
|
|
189
253
|
return;
|
|
190
254
|
}
|
|
191
255
|
if (!state().open && msg.operation === 'you_read') return;
|
|
192
|
-
if (!msg.ok) {
|
|
256
|
+
if (!msg.ok) {
|
|
257
|
+
if (msg.operation === 'you_read') showList();
|
|
258
|
+
if (msg.operation === 'you_delete' && deleteDialog) {
|
|
259
|
+
deleteDialog.querySelector('[data-action="delete"]').disabled = false;
|
|
260
|
+
deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
|
|
261
|
+
deleteDialog.querySelector('.you-delete-dialog-status').textContent = msg.error || 'The memory could not be deleted. Try again.';
|
|
262
|
+
}
|
|
263
|
+
status(msg.error || 'Clay could not complete this request.'); return;
|
|
264
|
+
}
|
|
193
265
|
status('');
|
|
194
266
|
var result = msg.result;
|
|
195
|
-
if (msg.operation === '
|
|
267
|
+
if (msg.operation === 'you_delete') {
|
|
268
|
+
closeDeleteDialog(true);
|
|
269
|
+
put({ deleteEntry: null });
|
|
270
|
+
showList();
|
|
271
|
+
element('you-search').focus();
|
|
272
|
+
requestAttention();
|
|
273
|
+
status('Memory deleted. Stored audit history remains.');
|
|
274
|
+
} else if (msg.operation === 'you_list') {
|
|
196
275
|
var entries = s.append ? (s.entries || []).concat(result.items) : result.items;
|
|
197
276
|
put({ entries: entries, nextOffset: result.nextOffset });
|
|
198
277
|
var emptyMessage = state().query || state().category
|
|
@@ -210,7 +289,7 @@ export function handleYouMessage(msg) {
|
|
|
210
289
|
element('you-detail').classList.remove('hidden'); element('you-back').classList.remove('hidden');
|
|
211
290
|
var previousInput = element('you-comment-input');
|
|
212
291
|
var draft = previousInput && previousInput.dataset.ref === result.ref ? previousInput.value : '';
|
|
213
|
-
renderDetail(element('you-detail'), result, { commentPrefix: 'you', commentReviewer: 'Clay', onComment: function (ref, body, feedback, input) {
|
|
292
|
+
renderDetail(element('you-detail'), result, { commentPrefix: 'you', commentReviewer: 'Clay', canDelete: !result.deleted, deleteLabel: 'Delete memory', onDelete: deleteEntry, onComment: function (ref, body, feedback, input) {
|
|
214
293
|
if (state().commentId) return;
|
|
215
294
|
put({ commentId: sendYou('you_comment', { ref: ref, body: body }), commentRef: ref });
|
|
216
295
|
input.form.querySelector('button').disabled = !!state().commentId;
|
|
@@ -236,13 +315,19 @@ export function initYou() {
|
|
|
236
315
|
element('you-button').addEventListener('click', function () { openYou(); });
|
|
237
316
|
store.subscribe(function (next, previous) {
|
|
238
317
|
if (next.currentSlug !== previous.currentSlug || next.myUserId !== previous.myUserId) {
|
|
239
|
-
closeYou(); clearTimeout(state().searchTimer);
|
|
318
|
+
closeDeleteDialog(true); closeYou(); clearTimeout(state().searchTimer);
|
|
240
319
|
if (next.myUserId !== previous.myUserId) { if (element('you-panel')) element('you-panel').remove(); put({ entries: [], query: '', category: '', attentionVersion: 0, acknowledged: null, attentionId: null, seenId: null }); showAttention({ unread: 0, version: 0 }); }
|
|
241
|
-
put({ listId: null, readId: null, commentId: null, retryId: null, seenId: null });
|
|
320
|
+
put({ listId: null, readId: null, commentId: null, retryId: null, seenId: null, deleteId: null, deleteEntry: null });
|
|
242
321
|
if (element('you-comment-input')) element('you-comment-input').form.querySelector('button').disabled = false;
|
|
243
322
|
}
|
|
244
323
|
if (previous.connected && !next.connected) {
|
|
245
324
|
put({ commentId: null, seenId: null, attentionId: null });
|
|
325
|
+
if (state().deleteId && deleteDialog) {
|
|
326
|
+
put({ deleteId: null });
|
|
327
|
+
deleteDialog.querySelector('[data-action="delete"]').disabled = false;
|
|
328
|
+
deleteDialog.querySelector('[data-action="cancel"]').disabled = false;
|
|
329
|
+
deleteDialog.querySelector('.you-delete-dialog-status').textContent = 'Disconnected. Reconnect before deleting. Stored audit history remains.';
|
|
330
|
+
}
|
|
246
331
|
if (element('you-comment-input')) element('you-comment-input').form.querySelector('button').disabled = false;
|
|
247
332
|
if (state().open) status('Disconnected. Reconnect before sending or refreshing.');
|
|
248
333
|
}
|
package/lib/public/style.css
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
@import url("css/stt.css");
|
|
48
48
|
@import url("css/profile.css");
|
|
49
49
|
@import url("css/user-settings.css?v=20260828-type1");
|
|
50
|
+
@import url("css/user-settings-sessions.css");
|
|
50
51
|
@import url("css/settings-ui.css");
|
|
51
52
|
@import url("css/admin.css");
|
|
52
53
|
@import url("css/session-search.css");
|