- Create Economist SubmissionTracking package correctly: * mainArticle = full blog post content * coverLetter = 216-word SIR— letter * Links to blog post via blogPostId - Archive 'Letter to The Economist' from blog posts (it's the cover letter) - Fix date display on article cards (use published_at) - Target publication already displaying via blue badge Database changes: - Make blogPostId optional in SubmissionTracking model - Economist package ID: 68fa85ae49d4900e7f2ecd83 - Le Monde package ID: 68fa2abd2e6acd5691932150 Next: Enhanced modal with tabs, validation, export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
736 lines
24 KiB
JavaScript
736 lines
24 KiB
JavaScript
/**
|
||
* Tractatus Credential Vault - Frontend Client
|
||
*
|
||
* WebSocket client for interactive credential vault access
|
||
*/
|
||
|
||
class VaultClient {
|
||
constructor() {
|
||
this.ws = null;
|
||
this.sessionId = null;
|
||
this.locked = true;
|
||
this.credentials = [];
|
||
this.autoLockTimer = null;
|
||
this.currentProject = 'tractatus'; // Default project
|
||
this.editingCredential = null; // Track credential being edited
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.connectWebSocket();
|
||
this.setupEventListeners();
|
||
this.resetAutoLockTimer();
|
||
}
|
||
|
||
connectWebSocket() {
|
||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
const host = window.location.hostname || '127.0.0.1';
|
||
const port = window.location.port || '8888';
|
||
|
||
this.ws = new WebSocket(`${protocol}//${host}:${port}`);
|
||
|
||
this.ws.onopen = () => {
|
||
console.log('WebSocket connected');
|
||
this.updateConnectionStatus(true);
|
||
};
|
||
|
||
this.ws.onmessage = (event) => {
|
||
this.handleMessage(JSON.parse(event.data));
|
||
};
|
||
|
||
this.ws.onerror = (error) => {
|
||
console.error('WebSocket error:', error);
|
||
this.showError('Connection error. Is the server running?');
|
||
};
|
||
|
||
this.ws.onclose = () => {
|
||
console.log('WebSocket disconnected');
|
||
this.updateConnectionStatus(false);
|
||
|
||
// Attempt to reconnect after 3 seconds
|
||
setTimeout(() => {
|
||
if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
||
this.connectWebSocket();
|
||
}
|
||
}, 3000);
|
||
};
|
||
}
|
||
|
||
send(data) {
|
||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||
this.ws.send(JSON.stringify(data));
|
||
this.resetAutoLockTimer();
|
||
} else {
|
||
this.showError('Not connected to server');
|
||
}
|
||
}
|
||
|
||
handleMessage(data) {
|
||
console.log('WebSocket message received:', data);
|
||
|
||
switch (data.type) {
|
||
case 'unlocked':
|
||
this.handleUnlocked(data);
|
||
break;
|
||
|
||
case 'locked':
|
||
this.handleLocked(data);
|
||
break;
|
||
|
||
case 'logged_out':
|
||
this.handleLoggedOut(data);
|
||
break;
|
||
|
||
case 'list':
|
||
this.handleList(data);
|
||
break;
|
||
|
||
case 'show':
|
||
this.handleShow(data);
|
||
break;
|
||
|
||
case 'totp':
|
||
this.handleTotp(data);
|
||
break;
|
||
|
||
case 'added':
|
||
this.handleAdded(data);
|
||
break;
|
||
|
||
case 'edited':
|
||
this.handleEdited(data);
|
||
break;
|
||
|
||
case 'deleted':
|
||
this.handleDeleted(data);
|
||
break;
|
||
|
||
case 'error':
|
||
this.showError(data.message);
|
||
break;
|
||
|
||
default:
|
||
console.warn('Unknown message type:', data.type);
|
||
}
|
||
}
|
||
|
||
handleUnlocked(data) {
|
||
this.sessionId = data.sessionId;
|
||
this.locked = false;
|
||
|
||
this.updateLockStatus(false);
|
||
this.showSuccess(data.message);
|
||
|
||
// Hide unlock form, show credentials
|
||
document.getElementById('unlock-section').classList.add('hidden');
|
||
document.getElementById('credentials-section').classList.remove('hidden');
|
||
|
||
// Load credentials
|
||
this.loadCredentials();
|
||
|
||
// Enable lock button
|
||
document.getElementById('lock-btn').disabled = false;
|
||
}
|
||
|
||
handleLocked(data) {
|
||
this.locked = true;
|
||
this.sessionId = null;
|
||
|
||
this.updateLockStatus(true);
|
||
this.showWarning(data.message);
|
||
|
||
// Show unlock form, hide credentials
|
||
document.getElementById('unlock-section').classList.remove('hidden');
|
||
document.getElementById('credentials-section').classList.add('hidden');
|
||
|
||
// Disable lock button
|
||
document.getElementById('lock-btn').disabled = true;
|
||
|
||
// Clear password input
|
||
document.getElementById('master-password').value = '';
|
||
}
|
||
|
||
handleLoggedOut(data) {
|
||
this.handleLocked(data);
|
||
}
|
||
|
||
handleList(data) {
|
||
this.credentials = data.entries;
|
||
this.renderCredentials();
|
||
}
|
||
|
||
handleShow(data) {
|
||
const credential = data.credential;
|
||
const entryName = data.entry.split('/').pop();
|
||
|
||
// Check if this is for editing
|
||
if (this.editingCredential && this.editingCredential === entryName) {
|
||
// Open edit modal with credential data
|
||
this.openCredentialModal('edit', {
|
||
name: entryName,
|
||
username: credential.username || '',
|
||
password: credential.password || '',
|
||
url: credential.url || '',
|
||
notes: credential.notes || ''
|
||
});
|
||
this.editingCredential = null; // Reset
|
||
return;
|
||
}
|
||
|
||
// Find credential card
|
||
const cards = document.querySelectorAll('.credential-card');
|
||
cards.forEach(card => {
|
||
if (card.dataset.entry === data.entry) {
|
||
const body = card.querySelector('.credential-body');
|
||
|
||
// Clear existing content
|
||
body.innerHTML = '';
|
||
|
||
// Add fields
|
||
for (const [key, value] of Object.entries(credential)) {
|
||
if (key === 'title') continue; // Skip title, already in header
|
||
|
||
const fieldDiv = document.createElement('div');
|
||
fieldDiv.className = `credential-field ${key === 'password' ? 'password' : ''}`;
|
||
|
||
const label = document.createElement('label');
|
||
label.textContent = key.charAt(0).toUpperCase() + key.slice(1) + ':';
|
||
|
||
const span = document.createElement('span');
|
||
span.textContent = value;
|
||
|
||
fieldDiv.appendChild(label);
|
||
fieldDiv.appendChild(span);
|
||
body.appendChild(fieldDiv);
|
||
}
|
||
|
||
// Add action buttons
|
||
const actionsDiv = document.createElement('div');
|
||
actionsDiv.className = 'credential-actions';
|
||
|
||
// Copy password button
|
||
if (credential.password) {
|
||
const copyBtn = document.createElement('button');
|
||
copyBtn.className = 'btn-copy';
|
||
copyBtn.textContent = 'Copy Password';
|
||
copyBtn.onclick = () => this.copyToClipboard(credential.password);
|
||
actionsDiv.appendChild(copyBtn);
|
||
}
|
||
|
||
// Copy username button
|
||
if (credential.username) {
|
||
const copyUserBtn = document.createElement('button');
|
||
copyUserBtn.className = 'btn-copy';
|
||
copyUserBtn.textContent = 'Copy Username';
|
||
copyUserBtn.onclick = () => this.copyToClipboard(credential.username);
|
||
actionsDiv.appendChild(copyUserBtn);
|
||
}
|
||
|
||
body.appendChild(actionsDiv);
|
||
|
||
// Show body
|
||
body.classList.add('visible');
|
||
}
|
||
});
|
||
}
|
||
|
||
handleTotp(data) {
|
||
const entryName = data.entry.split('/').pop();
|
||
|
||
// Find credential card and show TOTP
|
||
const cards = document.querySelectorAll('.credential-card');
|
||
cards.forEach(card => {
|
||
if (card.dataset.entry === data.entry) {
|
||
const body = card.querySelector('.credential-body');
|
||
|
||
// Add TOTP display
|
||
const totpDiv = document.createElement('div');
|
||
totpDiv.innerHTML = `
|
||
<div class="totp-code">${data.code}</div>
|
||
<div class="totp-timer">Expires in ${data.expiresIn} seconds</div>
|
||
`;
|
||
|
||
body.innerHTML = '';
|
||
body.appendChild(totpDiv);
|
||
body.classList.add('visible');
|
||
|
||
// Auto-refresh TOTP after expiry
|
||
setTimeout(() => {
|
||
if (body.classList.contains('visible')) {
|
||
this.send({ type: 'totp', entry: data.entry });
|
||
}
|
||
}, data.expiresIn * 1000);
|
||
}
|
||
});
|
||
}
|
||
|
||
handleAdded(data) {
|
||
console.log('handleAdded called:', data);
|
||
this.showSuccess(data.message);
|
||
this.closeCredentialModal();
|
||
// Reload credentials list
|
||
this.loadCredentials();
|
||
}
|
||
|
||
handleEdited(data) {
|
||
console.log('handleEdited called:', data);
|
||
this.showSuccess(data.message);
|
||
this.closeCredentialModal();
|
||
// Reload credentials list
|
||
this.loadCredentials();
|
||
}
|
||
|
||
handleDeleted(data) {
|
||
this.showSuccess(data.message);
|
||
// Reload credentials list
|
||
this.loadCredentials();
|
||
}
|
||
|
||
loadCredentials() {
|
||
this.send({ type: 'list', folder: `/${this.currentProject}` });
|
||
}
|
||
|
||
renderCredentials() {
|
||
const listEl = document.getElementById('credentials-list');
|
||
listEl.innerHTML = '';
|
||
|
||
if (this.credentials.length === 0) {
|
||
listEl.innerHTML = `<p class="warning">No credentials found in /${this.currentProject} folder</p>`;
|
||
return;
|
||
}
|
||
|
||
// Group credentials by type
|
||
const groups = {};
|
||
this.credentials.forEach(entry => {
|
||
const type = this.guessCredentialType(entry);
|
||
if (!groups[type]) {
|
||
groups[type] = [];
|
||
}
|
||
groups[type].push(entry);
|
||
});
|
||
|
||
// Sort credentials alphabetically within each group
|
||
Object.keys(groups).forEach(type => {
|
||
groups[type].sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
||
});
|
||
|
||
// Define display order for groups
|
||
const groupOrder = [
|
||
'Database',
|
||
'API Key',
|
||
'Secret',
|
||
'Auth',
|
||
'Email',
|
||
'SSH',
|
||
'2FA',
|
||
'URL',
|
||
'Config',
|
||
'Other'
|
||
];
|
||
|
||
// Render groups in order
|
||
groupOrder.forEach(type => {
|
||
if (!groups[type] || groups[type].length === 0) return;
|
||
|
||
// Create group header
|
||
const groupHeader = document.createElement('div');
|
||
groupHeader.className = 'credential-group-header';
|
||
groupHeader.innerHTML = `
|
||
<h2>${this.getGroupIcon(type)} ${type} <span class="count">(${groups[type].length})</span></h2>
|
||
`;
|
||
listEl.appendChild(groupHeader);
|
||
|
||
// Create group container
|
||
const groupContainer = document.createElement('div');
|
||
groupContainer.className = 'credential-group';
|
||
|
||
// Add credentials in this group
|
||
groups[type].forEach(entry => {
|
||
const card = this.createCredentialCard(entry);
|
||
groupContainer.appendChild(card);
|
||
});
|
||
|
||
listEl.appendChild(groupContainer);
|
||
});
|
||
}
|
||
|
||
getGroupIcon(type) {
|
||
const icons = {
|
||
'Database': '🗄️',
|
||
'API Key': '🔑',
|
||
'Secret': '🔐',
|
||
'Auth': '👤',
|
||
'Email': '📧',
|
||
'SSH': '🔒',
|
||
'2FA': '📱',
|
||
'URL': '🌐',
|
||
'Config': '⚙️',
|
||
'Other': '📦'
|
||
};
|
||
return icons[type] || '📦';
|
||
}
|
||
|
||
createCredentialCard(entry) {
|
||
const card = document.createElement('div');
|
||
card.className = 'credential-card';
|
||
card.dataset.entry = `/${this.currentProject}/${entry}`;
|
||
|
||
const header = document.createElement('div');
|
||
header.className = 'credential-header';
|
||
|
||
const title = document.createElement('h3');
|
||
title.textContent = `📦 ${entry}`;
|
||
|
||
const type = document.createElement('span');
|
||
type.className = 'credential-type';
|
||
type.textContent = this.guessCredentialType(entry);
|
||
|
||
// Action buttons
|
||
const actions = document.createElement('div');
|
||
actions.className = 'credential-actions';
|
||
|
||
const editBtn = document.createElement('button');
|
||
editBtn.className = 'btn-icon';
|
||
editBtn.innerHTML = '✏️';
|
||
editBtn.title = 'Edit credential';
|
||
editBtn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
// Set editing mode and fetch credential data
|
||
this.editingCredential = entry;
|
||
this.send({ type: 'show', entry: `/${this.currentProject}/${entry}` });
|
||
};
|
||
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'btn-icon btn-danger';
|
||
deleteBtn.innerHTML = '🗑️';
|
||
deleteBtn.title = 'Delete credential';
|
||
deleteBtn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
this.deleteCredential(entry);
|
||
};
|
||
|
||
actions.appendChild(editBtn);
|
||
actions.appendChild(deleteBtn);
|
||
|
||
header.appendChild(title);
|
||
header.appendChild(type);
|
||
header.appendChild(actions);
|
||
|
||
const body = document.createElement('div');
|
||
body.className = 'credential-body';
|
||
|
||
card.appendChild(header);
|
||
card.appendChild(body);
|
||
|
||
// Click to expand/show credential
|
||
card.onclick = () => {
|
||
if (body.classList.contains('visible')) {
|
||
body.classList.remove('visible');
|
||
} else {
|
||
// Hide all other cards
|
||
document.querySelectorAll('.credential-body').forEach(b => {
|
||
b.classList.remove('visible');
|
||
});
|
||
|
||
// Load this credential
|
||
this.send({ type: 'show', entry: `/${this.currentProject}/${entry}` });
|
||
}
|
||
};
|
||
|
||
return card;
|
||
}
|
||
|
||
guessCredentialType(name) {
|
||
const lower = name.toLowerCase();
|
||
|
||
// Database credentials
|
||
if (lower.includes('mongodb') || lower.includes('mongo_uri')) return 'Database';
|
||
if (lower.includes('db_') || lower.includes('database')) return 'Database';
|
||
if (lower.includes('postgres') || lower.includes('mysql')) return 'Database';
|
||
|
||
// API Keys & Tokens
|
||
if (lower.includes('api_key') || lower.includes('apikey')) return 'API Key';
|
||
if (lower.includes('stripe_') || lower.includes('anthropic')) return 'API Key';
|
||
if (lower.includes('token') || lower.includes('access_key')) return 'API Key';
|
||
|
||
// Secrets & Encryption
|
||
if (lower.includes('jwt_secret') || lower.includes('session_secret')) return 'Secret';
|
||
if (lower.includes('encryption') || lower.includes('cipher')) return 'Secret';
|
||
if (lower.includes('_secret') && !lower.includes('api')) return 'Secret';
|
||
|
||
// Authentication
|
||
if (lower.includes('password') || lower.includes('passwd')) return 'Auth';
|
||
if (lower.includes('username') || lower.includes('user_')) return 'Auth';
|
||
if (lower.includes('auth') || lower.includes('oauth')) return 'Auth';
|
||
|
||
// Email & SMTP
|
||
if (lower.includes('smtp') || lower.includes('email')) return 'Email';
|
||
if (lower.includes('mail_') || lower.includes('mailgun')) return 'Email';
|
||
|
||
// SSH & Security
|
||
if (lower.includes('ssh')) return 'SSH';
|
||
if (lower.includes('2fa') || lower.includes('totp')) return '2FA';
|
||
|
||
// URLs & Endpoints
|
||
if (lower.includes('url') || lower.includes('endpoint')) return 'URL';
|
||
if (lower.includes('_uri') || lower.includes('base_url')) return 'URL';
|
||
|
||
// Environment/Config
|
||
if (lower.includes('node_env') || lower.includes('port')) return 'Config';
|
||
if (lower.includes('host') || lower.includes('domain')) return 'Config';
|
||
if (lower.includes('log_') || lower.includes('debug')) return 'Config';
|
||
|
||
return 'Other';
|
||
}
|
||
|
||
copyToClipboard(text) {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
this.showSuccess('Copied to clipboard!');
|
||
|
||
// Auto-clear clipboard after 30 seconds
|
||
setTimeout(() => {
|
||
navigator.clipboard.writeText('');
|
||
}, 30000);
|
||
}).catch(err => {
|
||
this.showError('Failed to copy to clipboard');
|
||
});
|
||
}
|
||
|
||
updateLockStatus(locked) {
|
||
const statusEl = document.getElementById('lock-status');
|
||
|
||
if (locked) {
|
||
statusEl.textContent = '🔒 Locked';
|
||
statusEl.className = 'lock-status locked';
|
||
} else {
|
||
statusEl.textContent = '🔓 Unlocked';
|
||
statusEl.className = 'lock-status unlocked';
|
||
}
|
||
}
|
||
|
||
updateConnectionStatus(connected) {
|
||
const statusEl = document.getElementById('connection-status');
|
||
|
||
if (connected) {
|
||
statusEl.textContent = '✓ Connected';
|
||
statusEl.className = 'connection-status connected';
|
||
} else {
|
||
statusEl.textContent = '⚠️ Disconnected';
|
||
statusEl.className = 'connection-status disconnected';
|
||
}
|
||
}
|
||
|
||
showError(message) {
|
||
const messageEl = document.getElementById('unlock-message');
|
||
messageEl.className = 'error';
|
||
messageEl.textContent = '❌ ' + message;
|
||
messageEl.style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
messageEl.style.display = 'none';
|
||
}, 5000);
|
||
}
|
||
|
||
showSuccess(message) {
|
||
const messageEl = document.getElementById('unlock-message');
|
||
messageEl.className = 'success';
|
||
messageEl.textContent = '✓ ' + message;
|
||
messageEl.style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
messageEl.style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
|
||
showWarning(message) {
|
||
const messageEl = document.getElementById('unlock-message');
|
||
messageEl.className = 'warning';
|
||
messageEl.textContent = '⚠️ ' + message;
|
||
messageEl.style.display = 'block';
|
||
|
||
setTimeout(() => {
|
||
messageEl.style.display = 'none';
|
||
}, 5000);
|
||
}
|
||
|
||
resetAutoLockTimer() {
|
||
if (this.autoLockTimer) {
|
||
clearTimeout(this.autoLockTimer);
|
||
}
|
||
|
||
// Auto-lock after 15 minutes of inactivity
|
||
this.autoLockTimer = setTimeout(() => {
|
||
if (!this.locked) {
|
||
this.send({ type: 'lock' });
|
||
}
|
||
}, 15 * 60 * 1000);
|
||
}
|
||
|
||
openCredentialModal(mode = 'add', credential = null) {
|
||
const modal = document.getElementById('credential-modal');
|
||
const form = document.getElementById('credential-form');
|
||
const title = document.getElementById('modal-title');
|
||
const nameField = document.getElementById('cred-name');
|
||
|
||
// Set modal title
|
||
title.textContent = mode === 'add' ? '➕ Add Credential' : '✏️ Edit Credential';
|
||
|
||
// Clear/populate form
|
||
form.dataset.mode = mode;
|
||
nameField.value = credential?.name || '';
|
||
document.getElementById('cred-username').value = credential?.username || '';
|
||
document.getElementById('cred-password').value = credential?.password || '';
|
||
document.getElementById('cred-url').value = credential?.url || '';
|
||
document.getElementById('cred-notes').value = credential?.notes || '';
|
||
|
||
// Store original name for edit mode
|
||
// In edit mode, make name field read-only (KeePassXC can't rename entries)
|
||
if (mode === 'edit' && credential) {
|
||
form.dataset.originalName = credential.name;
|
||
nameField.readOnly = true;
|
||
nameField.style.backgroundColor = '#f5f5f5';
|
||
nameField.style.cursor = 'not-allowed';
|
||
} else {
|
||
nameField.readOnly = false;
|
||
nameField.style.backgroundColor = '';
|
||
nameField.style.cursor = '';
|
||
}
|
||
|
||
modal.style.display = 'block';
|
||
}
|
||
|
||
closeCredentialModal() {
|
||
const modal = document.getElementById('credential-modal');
|
||
modal.style.display = 'none';
|
||
|
||
// Clear form
|
||
document.getElementById('credential-form').reset();
|
||
}
|
||
|
||
saveCredential() {
|
||
const form = document.getElementById('credential-form');
|
||
const mode = form.dataset.mode;
|
||
|
||
const name = document.getElementById('cred-name').value.trim();
|
||
const username = document.getElementById('cred-username').value.trim();
|
||
const password = document.getElementById('cred-password').value;
|
||
const url = document.getElementById('cred-url').value.trim();
|
||
const notes = document.getElementById('cred-notes').value.trim();
|
||
|
||
console.log('saveCredential called - mode:', mode);
|
||
console.log('Form data:', { name, username, password: password ? '***' : '(empty)', url, notes });
|
||
|
||
if (!name) {
|
||
this.showError('Credential name is required');
|
||
return;
|
||
}
|
||
|
||
if (!password && mode === 'add') {
|
||
this.showError('Password is required');
|
||
return;
|
||
}
|
||
|
||
// For edit mode, use the original name to identify the entry
|
||
const entryName = (mode === 'edit' && form.dataset.originalName)
|
||
? form.dataset.originalName
|
||
: name;
|
||
|
||
console.log('Using entry name:', entryName, '(original:', form.dataset.originalName, ')');
|
||
|
||
const data = {
|
||
type: mode,
|
||
folder: `/${this.currentProject}`,
|
||
name: entryName,
|
||
username: username,
|
||
password: password,
|
||
url: url,
|
||
notes: notes
|
||
};
|
||
|
||
console.log('Sending WebSocket message:', data);
|
||
this.send(data);
|
||
}
|
||
|
||
deleteCredential(name) {
|
||
if (!confirm(`Are you sure you want to delete "${name}"?\n\nThis action cannot be undone.`)) {
|
||
return;
|
||
}
|
||
|
||
this.send({
|
||
type: 'delete',
|
||
folder: `/${this.currentProject}`,
|
||
name: name
|
||
});
|
||
}
|
||
|
||
setupEventListeners() {
|
||
// Unlock form
|
||
document.getElementById('unlock-form').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
|
||
const password = document.getElementById('master-password').value;
|
||
|
||
if (!password) {
|
||
this.showError('Please enter master password');
|
||
return;
|
||
}
|
||
|
||
document.getElementById('unlock-btn').disabled = true;
|
||
document.getElementById('unlock-btn').textContent = 'Unlocking...';
|
||
|
||
this.send({ type: 'unlock', password: password });
|
||
|
||
setTimeout(() => {
|
||
document.getElementById('unlock-btn').disabled = false;
|
||
document.getElementById('unlock-btn').textContent = 'Unlock Vault';
|
||
}, 2000);
|
||
});
|
||
|
||
// Lock button
|
||
document.getElementById('lock-btn').addEventListener('click', () => {
|
||
this.send({ type: 'lock' });
|
||
});
|
||
|
||
// Add credential button
|
||
document.getElementById('add-credential-btn').addEventListener('click', () => {
|
||
this.openCredentialModal('add');
|
||
});
|
||
|
||
// Project selector
|
||
document.getElementById('project-selector').addEventListener('change', (e) => {
|
||
this.currentProject = e.target.value;
|
||
this.loadCredentials();
|
||
});
|
||
|
||
// Modal close button
|
||
document.getElementById('modal-close-btn').addEventListener('click', () => {
|
||
this.closeCredentialModal();
|
||
});
|
||
|
||
// Modal cancel button
|
||
document.getElementById('modal-cancel-btn').addEventListener('click', () => {
|
||
this.closeCredentialModal();
|
||
});
|
||
|
||
// Modal save button
|
||
document.getElementById('modal-save-btn').addEventListener('click', () => {
|
||
this.saveCredential();
|
||
});
|
||
|
||
// Close modal on outside click
|
||
window.addEventListener('click', (e) => {
|
||
const modal = document.getElementById('credential-modal');
|
||
if (e.target === modal) {
|
||
this.closeCredentialModal();
|
||
}
|
||
});
|
||
|
||
// Reset auto-lock timer on user activity
|
||
document.addEventListener('mousemove', () => this.resetAutoLockTimer());
|
||
document.addEventListener('keypress', () => this.resetAutoLockTimer());
|
||
}
|
||
}
|
||
|
||
// Initialize vault client when page loads
|
||
const vaultClient = new VaultClient();
|