feat: Implement SigSocket request queuing and approval system, Enhance Settings UI

This commit is contained in:
zaelgohary 2025-06-17 03:18:25 +03:00
parent c641d0ae2e
commit 4f3f98a954
7 changed files with 1567 additions and 220 deletions

View File

@ -29,7 +29,21 @@ function startSessionTimeout() {
if (vault && currentSession) { if (vault && currentSession) {
// Lock the session // Lock the session
vault.lock_session(); vault.lock_session();
// Keep the session info for SigSocket connection but mark it as timed out
const keyspace = currentSession.keyspace;
await sessionManager.clear(); await sessionManager.clear();
// Maintain SigSocket connection for the locked keyspace to receive pending requests
if (sigSocketService && keyspace) {
try {
// Keep SigSocket connected to receive requests even when locked
console.log(`🔒 Session timed out but maintaining SigSocket connection for: ${keyspace}`);
} catch (error) {
console.warn('Failed to maintain SigSocket connection after timeout:', error);
}
}
// Notify popup if it's open // Notify popup if it's open
if (popupPort) { if (popupPort) {
popupPort.postMessage({ popupPort.postMessage({
@ -130,12 +144,48 @@ async function restoreSession() {
if (isUnlocked) { if (isUnlocked) {
// Restart keep-alive for restored session // Restart keep-alive for restored session
startKeepAlive(); startKeepAlive();
// Connect to SigSocket for the restored session
if (sigSocketService) {
try {
const connected = await sigSocketService.connectToServer(session.keyspace);
if (connected) {
console.log(`🔗 SigSocket reconnected for restored workspace: ${session.keyspace}`);
}
} catch (error) {
// Don't show as warning if it's just "no workspace" - this is expected on fresh start
if (error.message && error.message.includes('Workspace not found')) {
console.log(` SigSocket connection skipped for restored session: No workspace available yet`);
} else {
console.warn('Failed to reconnect SigSocket for restored session:', error);
}
}
}
return session; return session;
} else { } else {
await sessionManager.clear(); // Session exists but is locked - still try to connect SigSocket to receive pending requests
if (sigSocketService && session.keyspace) {
try {
const connected = await sigSocketService.connectToServer(session.keyspace);
if (connected) {
console.log(`🔗 SigSocket connected for locked workspace: ${session.keyspace} (will queue requests)`);
}
} catch (error) {
// Don't show as warning if it's just "no workspace" - this is expected on fresh start
if (error.message && error.message.includes('Workspace not found')) {
console.log(` SigSocket connection skipped for locked session: No workspace available yet`);
} else {
console.warn('Failed to connect SigSocket for locked session:', error);
}
}
}
// Don't clear the session - keep it for SigSocket connection
// await sessionManager.clear();
} }
} }
return null; return session; // Return session even if locked, so we know which keyspace to use
} }
// Import WASM module functions and SigSocket service // Import WASM module functions and SigSocket service
@ -187,14 +237,31 @@ const messageHandlers = {
// Smart auto-connect to SigSocket when session is initialized // Smart auto-connect to SigSocket when session is initialized
if (sigSocketService) { if (sigSocketService) {
try { try {
console.log(`🔗 Initializing SigSocket connection for workspace: ${request.keyspace}`);
// This will reuse existing connection if same workspace, or switch if different // This will reuse existing connection if same workspace, or switch if different
const connected = await sigSocketService.connectToServer(request.keyspace); const connected = await sigSocketService.connectToServer(request.keyspace);
if (connected) { if (connected) {
console.log(`🔗 SigSocket ready for workspace: ${request.keyspace}`); console.log(`✅ SigSocket ready for workspace: ${request.keyspace}`);
} else {
console.warn(`⚠️ SigSocket connection failed for workspace: ${request.keyspace}`);
} }
} catch (error) { } catch (error) {
console.warn('Failed to auto-connect to SigSocket:', error); console.warn('Failed to auto-connect to SigSocket:', error);
// If connection fails, try once more after a short delay
setTimeout(async () => {
try {
console.log(`🔄 Retrying SigSocket connection for workspace: ${request.keyspace}`);
await sigSocketService.connectToServer(request.keyspace);
} catch (retryError) {
console.warn('SigSocket retry connection also failed:', retryError);
}
}, 2000);
} }
// Notify SigSocket service that keyspace is now unlocked
await sigSocketService.onKeypaceUnlocked();
} }
return { success: true }; return { success: true };
@ -288,6 +355,19 @@ const messageHandlers = {
return { success: true }; return { success: true };
}, },
updateSigSocketUrl: async (request) => {
if (sigSocketService) {
// Update the server URL in the SigSocket service
sigSocketService.defaultServerUrl = request.serverUrl;
// Save to storage (already done in popup, but ensure consistency)
await chrome.storage.local.set({ sigSocketUrl: request.serverUrl });
console.log(`🔗 SigSocket server URL updated to: ${request.serverUrl}`);
}
return { success: true };
},
// SigSocket handlers // SigSocket handlers
connectSigSocket: async (request) => { connectSigSocket: async (request) => {
if (!sigSocketService) { if (!sigSocketService) {
@ -313,6 +393,15 @@ const messageHandlers = {
return { success: true, status }; return { success: true, status };
}, },
getSigSocketStatusWithTest: async () => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
// Use the enhanced connection testing method
const status = await sigSocketService.getStatusWithConnectionTest();
return { success: true, status };
},
getPendingSignRequests: async () => { getPendingSignRequests: async () => {
if (!sigSocketService) { if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' }; return { success: false, error: 'SigSocket service not initialized' };
@ -393,6 +482,25 @@ chrome.runtime.onConnect.addListener((port) => {
startKeepAlive(); startKeepAlive();
} }
// Handle messages from popup
port.onMessage.addListener(async (message) => {
if (message.type === 'REQUEST_IMMEDIATE_STATUS') {
// Immediately send current SigSocket status to popup
if (sigSocketService) {
try {
const status = await sigSocketService.getStatus();
port.postMessage({
type: 'CONNECTION_STATUS_CHANGED',
status: status
});
console.log('📡 Sent immediate status to popup:', status.isConnected ? 'Connected' : 'Disconnected');
} catch (error) {
console.warn('Failed to send immediate status:', error);
}
}
}
});
port.onDisconnect.addListener(() => { port.onDisconnect.addListener(() => {
// Popup closed, clear reference and stop keep-alive // Popup closed, clear reference and stop keep-alive
popupPort = null; popupPort = null;
@ -405,3 +513,158 @@ chrome.runtime.onConnect.addListener((port) => {
}); });
} }
}); });
// Handle notification clicks to open extension (notifications are now clickable without buttons)
chrome.notifications.onClicked.addListener(async (notificationId) => {
console.log(`🔔 Notification clicked: ${notificationId}`);
// Check if this is a SigSocket notification
if (notificationId.startsWith('sigsocket-request-')) {
console.log('🔔 SigSocket notification clicked, opening extension...');
try {
await openExtensionPopup();
// Clear the notification after successfully opening
chrome.notifications.clear(notificationId);
console.log('✅ Notification cleared after opening extension');
} catch (error) {
console.error('❌ Failed to handle notification click:', error);
}
} else {
console.log('🔔 Non-SigSocket notification clicked, ignoring');
}
});
// Note: Notification button handler removed - notifications are now clickable without buttons
// Function to open extension popup with best UX
async function openExtensionPopup() {
try {
console.log('🔔 Opening extension popup from notification...');
// First, check if there's already a popup window open
const windows = await chrome.windows.getAll({ populate: true });
const existingPopup = windows.find(window =>
window.type === 'popup' &&
window.tabs?.some(tab => tab.url?.includes('popup.html'))
);
if (existingPopup) {
// Focus existing popup and send focus message
await chrome.windows.update(existingPopup.id, { focused: true });
console.log('✅ Focused existing popup window');
// Send message to focus on SigSocket section
if (popupPort) {
popupPort.postMessage({
type: 'FOCUS_SIGSOCKET',
fromNotification: true
});
}
return;
}
// Best UX: Try to use the normal popup experience
// The action API gives the same popup as clicking the extension icon
try {
if (chrome.action && chrome.action.openPopup) {
await chrome.action.openPopup();
console.log('✅ Extension popup opened via action API (best UX - normal popup)');
// Send focus message after popup opens
setTimeout(() => {
if (popupPort) {
popupPort.postMessage({
type: 'FOCUS_SIGSOCKET',
fromNotification: true
});
}
}, 200);
return;
}
} catch (actionError) {
// The action API fails when there's no active browser window
// This is common when all browser windows are closed but extension is still running
console.log('⚠️ Action API failed (likely no active window):', actionError.message);
// Check if we have any normal browser windows
const allWindows = await chrome.windows.getAll();
const normalWindows = allWindows.filter(w => w.type === 'normal');
if (normalWindows.length > 0) {
// We have browser windows, try to focus one and retry action API
try {
const targetWindow = normalWindows.find(w => w.focused) || normalWindows[0];
await chrome.windows.update(targetWindow.id, { focused: true });
// Small delay and retry
await new Promise(resolve => setTimeout(resolve, 100));
await chrome.action.openPopup();
console.log('✅ Extension popup opened via action API after focusing window');
setTimeout(() => {
if (popupPort) {
popupPort.postMessage({
type: 'FOCUS_SIGSOCKET',
fromNotification: true
});
}
}, 200);
return;
} catch (retryError) {
console.log('⚠️ Action API retry also failed:', retryError.message);
}
}
}
// If action API fails completely, we need to create a window
// But let's make it as close to the normal popup experience as possible
console.log('⚠️ Creating popup window as fallback (action API unavailable)');
const popupUrl = chrome.runtime.getURL('popup.html?from=notification');
// Position the popup where the extension icon would normally show its popup
// Try to position it in the top-right area like a normal extension popup
let left = screen.width - 420; // 400px width + 20px margin
let top = 80; // Below browser toolbar area
try {
// If we have a browser window, position relative to it
const allWindows = await chrome.windows.getAll();
const normalWindows = allWindows.filter(w => w.type === 'normal');
if (normalWindows.length > 0) {
const referenceWindow = normalWindows[0];
left = (referenceWindow.left || 0) + (referenceWindow.width || 800) - 420;
top = (referenceWindow.top || 0) + 80;
}
} catch (positionError) {
console.log('⚠️ Could not get window position, using screen-based positioning');
}
const newWindow = await chrome.windows.create({
url: popupUrl,
type: 'popup',
width: 400,
height: 600,
left: Math.max(0, left),
top: Math.max(0, top),
focused: true
});
console.log(`✅ Extension popup window created: ${newWindow.id}`);
} catch (error) {
console.error('❌ Failed to open extension popup:', error);
// Final fallback: open in new tab (least ideal but still functional)
try {
const popupUrl = chrome.runtime.getURL('popup.html?from=notification');
await chrome.tabs.create({ url: popupUrl, active: true });
console.log('✅ Opened extension in new tab as final fallback');
} catch (tabError) {
console.error('❌ All popup opening methods failed:', tabError);
}
}
}

View File

@ -25,6 +25,10 @@ class SigSocketService {
// UI communication // UI communication
this.popupPort = null; this.popupPort = null;
// Status monitoring
this.statusMonitorInterval = null;
this.lastKnownConnectionState = false;
} }
/** /**
@ -44,72 +48,221 @@ class SigSocketService {
console.warn('Failed to load SigSocket URL from storage:', error); console.warn('Failed to load SigSocket URL from storage:', error);
} }
// Restore any persisted pending requests
await this.restorePendingRequests();
console.log('🔌 SigSocket service initialized with WASM APIs'); console.log('🔌 SigSocket service initialized with WASM APIs');
} }
/**
* Restore pending requests from persistent storage
* Only restore requests that match the current workspace
*/
async restorePendingRequests() {
try {
const result = await chrome.storage.local.get(['sigSocketPendingRequests']);
if (result.sigSocketPendingRequests && Array.isArray(result.sigSocketPendingRequests)) {
console.log(`🔄 Found ${result.sigSocketPendingRequests.length} stored requests`);
// Filter requests for current workspace only
const currentWorkspaceRequests = result.sigSocketPendingRequests.filter(request =>
request.target_public_key === this.connectedPublicKey
);
console.log(`🔄 Restoring ${currentWorkspaceRequests.length} requests for current workspace`);
// Add each workspace-specific request back to WASM storage
for (const request of currentWorkspaceRequests) {
try {
await this.wasmModule.SigSocketManager.add_pending_request(JSON.stringify(request.request || request));
console.log(`✅ Restored request: ${request.id || request.request?.id}`);
} catch (error) {
console.warn(`Failed to restore request ${request.id || request.request?.id}:`, error);
}
}
// Update badge after restoration
this.updateBadge();
}
} catch (error) {
console.warn('Failed to restore pending requests:', error);
}
}
/**
* Persist pending requests to storage with workspace isolation
*/
async persistPendingRequests() {
try {
const requests = await this.getFilteredRequests();
// Get existing storage to merge with other workspaces
const result = await chrome.storage.local.get(['sigSocketPendingRequests']);
const existingRequests = result.sigSocketPendingRequests || [];
// Remove old requests for current workspace
const otherWorkspaceRequests = existingRequests.filter(request =>
request.target_public_key !== this.connectedPublicKey
);
// Combine with current workspace requests
const allRequests = [...otherWorkspaceRequests, ...requests];
await chrome.storage.local.set({ sigSocketPendingRequests: allRequests });
console.log(`💾 Persisted ${requests.length} requests for current workspace (${allRequests.length} total)`);
} catch (error) {
console.warn('Failed to persist pending requests:', error);
}
}
/** /**
* Connect to SigSocket server using WASM APIs * Connect to SigSocket server using WASM APIs
* WASM handles all connection logic (reuse, switching, etc.) * WASM handles all connection logic (reuse, switching, etc.)
* @param {string} workspaceId - The workspace/keyspace identifier * @param {string} workspaceId - The workspace/keyspace identifier
* @param {number} retryCount - Number of retry attempts (default: 3)
* @returns {Promise<boolean>} - True if connected successfully * @returns {Promise<boolean>} - True if connected successfully
*/ */
async connectToServer(workspaceId) { async connectToServer(workspaceId, retryCount = 3) {
try { for (let attempt = 1; attempt <= retryCount; attempt++) {
if (!this.wasmModule?.SigSocketManager) { try {
throw new Error('WASM SigSocketManager not available'); if (!this.wasmModule?.SigSocketManager) {
throw new Error('WASM SigSocketManager not available');
}
console.log(`🔗 Requesting SigSocket connection for workspace: ${workspaceId} (attempt ${attempt}/${retryCount})`);
// Clean workspace switching
if (this.currentWorkspace && this.currentWorkspace !== workspaceId) {
console.log(`🔄 Clean workspace switch: ${this.currentWorkspace} -> ${workspaceId}`);
await this.cleanWorkspaceSwitch(workspaceId);
// Small delay to ensure clean state transition
await new Promise(resolve => setTimeout(resolve, 300));
}
// Let WASM handle all connection logic (reuse, switching, etc.)
const connectionInfo = await this.wasmModule.SigSocketManager.connect_workspace_with_events(
workspaceId,
this.defaultServerUrl,
(event) => this.handleSigSocketEvent(event)
);
// Parse connection info
const info = JSON.parse(connectionInfo);
this.currentWorkspace = workspaceId; // Use the parameter we passed, not WASM response
this.connectedPublicKey = info.public_key;
this.isConnected = info.is_connected;
console.log(`✅ SigSocket connection result:`, {
workspace: this.currentWorkspace,
publicKey: this.connectedPublicKey?.substring(0, 16) + '...',
connected: this.isConnected,
serverUrl: this.defaultServerUrl
});
// Validate that we have a public key if connected
if (this.isConnected && !this.connectedPublicKey) {
console.warn('⚠️ Connected but no public key received - this may cause request issues');
}
// Update badge to show current state
this.updateBadge();
if (this.isConnected) {
// Clean flow: Connect -> Restore workspace requests -> Update UI
console.log(`🔗 Connected to workspace: ${workspaceId}, restoring pending requests...`);
// 1. Restore requests for this specific workspace
await this.restorePendingRequests();
// 2. Update badge with current count
this.updateBadge();
console.log(`✅ Workspace ${workspaceId} ready with restored requests`);
return true;
}
// If not connected but no error, try again
if (attempt < retryCount) {
console.log(`⏳ Connection not established, retrying in 1 second...`);
await new Promise(resolve => setTimeout(resolve, 1000));
}
} catch (error) {
// Check if this is an expected "no workspace" error during startup
const isExpectedStartupError = error.message &&
(error.message.includes('Workspace not found') ||
error.message.includes('no keypairs available'));
if (isExpectedStartupError && attempt === 1) {
console.log(`⏳ SigSocket connection attempt ${attempt}: No active workspace (expected after extension reload)`);
}
// Check if this is a public key related error
if (error.message && error.message.includes('public key')) {
console.error(`🔑 Public key error detected: ${error.message}`);
// For public key errors, don't retry immediately - might need workspace change
if (attempt === 1) {
console.log(`🔄 Public key error on first attempt, trying to disconnect and reconnect...`);
await this.disconnect();
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
if (attempt < retryCount) {
if (!isExpectedStartupError) {
console.log(`⏳ Retrying connection in 2 seconds...`);
}
await new Promise(resolve => setTimeout(resolve, 2000));
} else {
// Final attempt failed
this.isConnected = false;
this.currentWorkspace = null;
this.connectedPublicKey = null;
if (isExpectedStartupError) {
console.log(` SigSocket connection failed: No active workspace. Will connect when user logs in.`);
}
}
} }
console.log(`🔗 Requesting SigSocket connection for workspace: ${workspaceId}`);
// Let WASM handle all connection logic (reuse, switching, etc.)
const connectionInfo = await this.wasmModule.SigSocketManager.connect_workspace_with_events(
workspaceId,
this.defaultServerUrl,
(event) => this.handleSigSocketEvent(event)
);
// Parse connection info
const info = JSON.parse(connectionInfo);
this.currentWorkspace = info.workspace;
this.connectedPublicKey = info.public_key;
this.isConnected = info.is_connected;
console.log(`✅ SigSocket connection result:`, {
workspace: this.currentWorkspace,
publicKey: this.connectedPublicKey?.substring(0, 16) + '...',
connected: this.isConnected
});
// Update badge to show current state
this.updateBadge();
return this.isConnected;
} catch (error) {
console.error('❌ SigSocket connection failed:', error);
this.isConnected = false;
this.currentWorkspace = null;
this.connectedPublicKey = null;
return false;
} }
return false;
} }
/** /**
* Handle events from the WASM SigSocket client * Handle events from the WASM SigSocket client
* This is called automatically when requests arrive * This is called automatically when requests arrive
* @param {Object} event - Event from WASM layer * @param {Object} event - Event from WASM layer
*/ */
handleSigSocketEvent(event) { async handleSigSocketEvent(event) {
console.log('📨 Received SigSocket event:', event); console.log('📨 Received SigSocket event:', event);
if (event.type === 'sign_request') { if (event.type === 'sign_request') {
console.log(`🔐 New sign request: ${event.request_id}`); console.log(`🔐 New sign request: ${event.request_id} for workspace: ${this.currentWorkspace}`);
// The request is automatically stored by WASM // Clean flow: Request arrives -> Store -> Persist -> Update UI
// We just handle UI updates try {
this.showSignRequestNotification(); // 1. Request is automatically stored in WASM (already done by WASM layer)
this.updateBadge();
this.notifyPopupOfNewRequest(); // 2. Persist to storage with workspace isolation
await this.persistPendingRequests();
// 3. Update badge count
this.updateBadge();
// 4. Show notification
this.showSignRequestNotification();
// 5. Notify popup if connected
this.notifyPopupOfNewRequest();
console.log(`✅ Request ${event.request_id} processed and stored for workspace: ${this.currentWorkspace}`);
} catch (error) {
console.error(`❌ Failed to process request ${event.request_id}:`, error);
}
} }
} }
@ -124,6 +277,19 @@ class SigSocketService {
throw new Error('WASM SigSocketManager not available'); throw new Error('WASM SigSocketManager not available');
} }
// Check if we're connected before attempting approval
if (!this.isConnected) {
console.warn(`⚠️ Not connected to SigSocket server, cannot approve request: ${requestId}`);
throw new Error('Not connected to SigSocket server');
}
// Verify we can approve this request
const canApprove = await this.canApproveRequest(requestId);
if (!canApprove) {
console.warn(`⚠️ Cannot approve request ${requestId} - keyspace may be locked or request not found`);
throw new Error('Cannot approve request - keyspace may be locked or request not found');
}
console.log(`✅ Approving request: ${requestId}`); console.log(`✅ Approving request: ${requestId}`);
// WASM handles all validation, signing, and server communication // WASM handles all validation, signing, and server communication
@ -131,14 +297,37 @@ class SigSocketService {
console.log(`🎉 Request approved successfully: ${requestId}`); console.log(`🎉 Request approved successfully: ${requestId}`);
// Update UI // Clean flow: Approve -> Remove from storage -> Update UI
// 1. Remove from persistent storage (WASM already removed it)
await this.persistPendingRequests();
// 2. Update badge count
this.updateBadge(); this.updateBadge();
// 3. Notify popup of updated state
this.notifyPopupOfRequestUpdate(); this.notifyPopupOfRequestUpdate();
console.log(`✅ Request ${requestId} approved and cleaned up`);
return true; return true;
} catch (error) { } catch (error) {
console.error(`❌ Failed to approve request ${requestId}:`, error); console.error(`❌ Failed to approve request ${requestId}:`, error);
// Check if this is a connection-related error
if (error.message && (error.message.includes('Connection not found') || error.message.includes('public key'))) {
console.error(`🔑 Connection/public key error during approval. Current state:`, {
connected: this.isConnected,
workspace: this.currentWorkspace,
publicKey: this.connectedPublicKey?.substring(0, 16) + '...'
});
// Try to reconnect for next time
if (this.currentWorkspace) {
console.log(`🔄 Attempting to reconnect to workspace: ${this.currentWorkspace}`);
setTimeout(() => this.connectToServer(this.currentWorkspace), 1000);
}
}
return false; return false;
} }
} }
@ -162,10 +351,17 @@ class SigSocketService {
console.log(`✅ Request rejected successfully: ${requestId}`); console.log(`✅ Request rejected successfully: ${requestId}`);
// Update UI // Clean flow: Reject -> Remove from storage -> Update UI
// 1. Remove from persistent storage (WASM already removed it)
await this.persistPendingRequests();
// 2. Update badge count
this.updateBadge(); this.updateBadge();
// 3. Notify popup of updated state
this.notifyPopupOfRequestUpdate(); this.notifyPopupOfRequestUpdate();
console.log(`✅ Request ${requestId} rejected and cleaned up`);
return true; return true;
} catch (error) { } catch (error) {
@ -224,18 +420,54 @@ class SigSocketService {
} }
/** /**
* Show notification for new sign request * Show clickable notification for new sign request
* Call this AFTER the request has been stored and persisted
*/ */
showSignRequestNotification() { async showSignRequestNotification() {
try { try {
if (chrome.notifications && chrome.notifications.create) { if (chrome.notifications && chrome.notifications.create) {
chrome.notifications.create({ // Small delay to ensure request is fully stored
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`📢 Preparing notification for new signature request`);
// Check if keyspace is currently unlocked to customize message
let message = 'New signature request received. Click to review and approve.';
let title = 'SigSocket Sign Request';
// Try to determine if keyspace is locked
try {
const requests = await this.getPendingRequests();
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
if (!canApprove) {
message = 'New signature request received. Click to unlock keyspace and approve.';
title = 'SigSocket Request';
}
} catch (error) {
// If we can't check, use generic message
message = 'New signature request received. Click to open extension.';
}
// Create clickable notification with unique ID
const notificationId = `sigsocket-request-${Date.now()}`;
const notificationOptions = {
type: 'basic', type: 'basic',
iconUrl: 'icons/icon48.png', iconUrl: 'icons/icon48.png',
title: 'SigSocket Sign Request', title: title,
message: 'New signature request received. Click to review.' message: message,
requireInteraction: true // Keep notification visible until user interacts
};
console.log(`📢 Creating notification: ${notificationId}`, notificationOptions);
chrome.notifications.create(notificationId, notificationOptions, (createdId) => {
if (chrome.runtime.lastError) {
console.error('❌ Failed to create notification:', chrome.runtime.lastError);
} else {
console.log(`✅ Notification created successfully: ${createdId}`);
}
}); });
console.log('📢 Notification shown for sign request');
} else { } else {
console.log('📢 Notifications not available, skipping notification'); console.log('📢 Notifications not available, skipping notification');
} }
@ -322,6 +554,10 @@ class SigSocketService {
this.isConnected = false; this.isConnected = false;
this.currentWorkspace = null; this.currentWorkspace = null;
this.connectedPublicKey = null; this.connectedPublicKey = null;
this.lastKnownConnectionState = false;
// Stop status monitoring
this.stopStatusMonitoring();
this.updateBadge(); this.updateBadge();
@ -333,7 +569,50 @@ class SigSocketService {
} }
/** /**
* Get connection status from WASM * Clear persisted pending requests from storage
*/
async clearPersistedRequests() {
try {
await chrome.storage.local.remove(['sigSocketPendingRequests']);
console.log('🗑️ Cleared persisted pending requests from storage');
} catch (error) {
console.warn('Failed to clear persisted requests:', error);
}
}
/**
* Clean workspace switch - clear current workspace requests only
*/
async cleanWorkspaceSwitch(newWorkspace) {
try {
console.log(`🔄 Clean workspace switch: ${this.currentWorkspace} -> ${newWorkspace}`);
// 1. Persist current workspace requests before switching
if (this.currentWorkspace && this.isConnected) {
await this.persistPendingRequests();
console.log(`💾 Saved requests for workspace: ${this.currentWorkspace}`);
}
// 2. Clear WASM state (will be restored for new workspace)
if (this.wasmModule?.SigSocketManager) {
await this.wasmModule.SigSocketManager.clear_pending_requests();
console.log('🧹 Cleared WASM request state');
}
// 3. Reset local state
this.currentWorkspace = null;
this.connectedPublicKey = null;
this.isConnected = false;
console.log('✅ Workspace switch cleanup completed');
} catch (error) {
console.error('❌ Failed to clean workspace switch:', error);
}
}
/**
* Get connection status with real connection verification
* @returns {Promise<Object>} - Connection status information * @returns {Promise<Object>} - Connection status information
*/ */
async getStatus() { async getStatus() {
@ -348,21 +627,63 @@ class SigSocketService {
}; };
} }
// Let WASM provide the authoritative status // Get WASM status first
const statusJson = await this.wasmModule.SigSocketManager.get_connection_status(); const statusJson = await this.wasmModule.SigSocketManager.get_connection_status();
const status = JSON.parse(statusJson); const status = JSON.parse(statusJson);
const requests = await this.getPendingRequests();
return { // Verify connection by trying to get requests (this will fail if not connected)
isConnected: status.is_connected, let actuallyConnected = false;
workspace: status.workspace, let requests = [];
publicKey: status.public_key,
try {
requests = await this.getPendingRequests();
// If we can get requests and WASM says connected, we're probably connected
actuallyConnected = status.is_connected && Array.isArray(requests);
} catch (error) {
// If getting requests fails, we're definitely not connected
console.warn('Connection verification failed:', error);
actuallyConnected = false;
}
// Update our internal state
this.isConnected = actuallyConnected;
if (status.connected_public_key && actuallyConnected) {
this.connectedPublicKey = status.connected_public_key;
} else {
this.connectedPublicKey = null;
}
// If we're disconnected, clear our workspace
if (!actuallyConnected) {
this.currentWorkspace = null;
}
const statusResult = {
isConnected: actuallyConnected,
workspace: this.currentWorkspace,
publicKey: status.connected_public_key,
pendingRequestCount: requests.length, pendingRequestCount: requests.length,
serverUrl: this.defaultServerUrl serverUrl: this.defaultServerUrl,
// Clean flow status indicators
cleanFlowReady: actuallyConnected && this.currentWorkspace && status.connected_public_key
}; };
console.log('📊 Clean flow status:', {
connected: statusResult.isConnected,
workspace: statusResult.workspace,
requestCount: statusResult.pendingRequestCount,
flowReady: statusResult.cleanFlowReady
});
return statusResult;
} catch (error) { } catch (error) {
console.error('Failed to get status:', error); console.error('Failed to get status:', error);
// Clear state on error
this.isConnected = false;
this.currentWorkspace = null;
this.connectedPublicKey = null;
return { return {
isConnected: false, isConnected: false,
workspace: null, workspace: null,
@ -375,33 +696,178 @@ class SigSocketService {
/** /**
* Set the popup port for communication * Set the popup port for communication
* @param {chrome.runtime.Port} port - The popup port * @param {chrome.runtime.Port|null} port - The popup port or null to disconnect
*/ */
setPopupPort(port) { setPopupPort(port) {
this.popupPort = port; this.popupPort = port;
console.log('📱 Popup connected to SigSocket service');
if (port) {
console.log('📱 Popup connected to SigSocket service');
// Immediately check connection status when popup opens
this.checkConnectionStatusNow();
// Start monitoring connection status when popup connects
this.startStatusMonitoring();
} else {
console.log('📱 Popup disconnected from SigSocket service');
// Stop monitoring when popup disconnects
this.stopStatusMonitoring();
}
} }
/** /**
* Called when keyspace is unlocked - notify popup of current state * Immediately check and update connection status
*/
async checkConnectionStatusNow() {
try {
// Force a fresh connection check
const currentStatus = await this.getStatusWithConnectionTest();
this.lastKnownConnectionState = currentStatus.isConnected;
// Notify popup of current status
this.notifyPopupOfStatusChange(currentStatus);
console.log(`🔍 Immediate status check: ${currentStatus.isConnected ? 'Connected' : 'Disconnected'}`);
} catch (error) {
console.warn('Failed to check connection status immediately:', error);
}
}
/**
* Get status with additional connection testing
*/
async getStatusWithConnectionTest() {
const status = await this.getStatus();
// If WASM claims we're connected, do an additional verification
if (status.isConnected) {
try {
// Try to get connection status again - if this fails, we're not really connected
const verifyJson = await this.wasmModule.SigSocketManager.get_connection_status();
const verifyStatus = JSON.parse(verifyJson);
if (!verifyStatus.is_connected) {
console.log('🔍 Connection verification failed - marking as disconnected');
status.isConnected = false;
this.isConnected = false;
this.currentWorkspace = null;
}
} catch (error) {
console.log('🔍 Connection test failed - marking as disconnected:', error.message);
status.isConnected = false;
this.isConnected = false;
this.currentWorkspace = null;
}
}
return status;
}
/**
* Start periodic status monitoring to detect connection changes
*/
startStatusMonitoring() {
// Clear any existing monitoring
if (this.statusMonitorInterval) {
clearInterval(this.statusMonitorInterval);
}
// Check status every 2 seconds when popup is open (more responsive)
this.statusMonitorInterval = setInterval(async () => {
if (this.popupPort) {
try {
const currentStatus = await this.getStatusWithConnectionTest();
// Check if connection status changed
if (currentStatus.isConnected !== this.lastKnownConnectionState) {
console.log(`🔄 Connection state changed: ${this.lastKnownConnectionState} -> ${currentStatus.isConnected}`);
this.lastKnownConnectionState = currentStatus.isConnected;
// Notify popup of status change
this.notifyPopupOfStatusChange(currentStatus);
}
} catch (error) {
console.warn('Status monitoring error:', error);
// On error, assume disconnected
if (this.lastKnownConnectionState !== false) {
console.log('🔄 Status monitoring error - marking as disconnected');
this.lastKnownConnectionState = false;
this.notifyPopupOfStatusChange({
isConnected: false,
workspace: null,
publicKey: null,
pendingRequestCount: 0,
serverUrl: this.defaultServerUrl
});
}
}
} else {
// Stop monitoring when popup is closed
this.stopStatusMonitoring();
}
}, 2000); // 2 seconds for better responsiveness
}
/**
* Stop status monitoring
*/
stopStatusMonitoring() {
if (this.statusMonitorInterval) {
clearInterval(this.statusMonitorInterval);
this.statusMonitorInterval = null;
}
}
/**
* Notify popup of connection status change
* @param {Object} status - Current connection status
*/
notifyPopupOfStatusChange(status) {
if (this.popupPort) {
this.popupPort.postMessage({
type: 'CONNECTION_STATUS_CHANGED',
status: status
});
console.log(`📡 Notified popup of connection status change: ${status.isConnected ? 'Connected' : 'Disconnected'}`);
}
}
/**
* Called when keyspace is unlocked - clean approach to show pending requests
*/ */
async onKeypaceUnlocked() { async onKeypaceUnlocked() {
if (!this.popupPort) return;
try { try {
console.log('🔓 Keyspace unlocked - preparing to show pending requests');
// 1. Restore any persisted requests for this workspace
await this.restorePendingRequests();
// 2. Get current requests (includes restored + any new ones)
const requests = await this.getPendingRequests(); const requests = await this.getPendingRequests();
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
this.popupPort.postMessage({ // 3. Check if we can approve requests (keyspace should be unlocked now)
type: 'KEYSPACE_UNLOCKED', const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : true;
canApprove,
pendingRequests: requests
});
console.log(`🔓 Keyspace unlocked notification sent: ${requests.length} requests, canApprove: ${canApprove}`); // 4. Update badge with current count
this.updateBadge();
// 5. Notify popup if connected
if (this.popupPort) {
this.popupPort.postMessage({
type: 'KEYSPACE_UNLOCKED',
canApprove,
pendingRequests: requests
});
}
console.log(`🔓 Keyspace unlocked: ${requests.length} requests ready, canApprove: ${canApprove}`);
return requests;
} catch (error) { } catch (error) {
console.error('Failed to handle keyspace unlock:', error); console.error('Failed to handle keyspace unlock:', error);
return [];
} }
} }
} }

View File

@ -7,28 +7,17 @@
<body> <body>
<div class="container"> <div class="container">
<header class="header"> <header class="header">
<div class="logo"> <div class="logo clickable-header" id="headerTitle">
<div class="logo-icon">🔐</div> <div class="logo-icon">🔐</div>
<h1>CryptoVault</h1> <h1>CryptoVault</h1>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<div class="settings-container"> <button id="settingsBtn" class="btn-icon-only" title="Settings">
<button id="settingsToggle" class="btn-icon-only" title="Settings"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="3"></circle>
<circle cx="12" cy="12" r="3"></circle> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> </svg>
</svg> </button>
</button>
<div class="settings-dropdown hidden" id="settingsDropdown">
<div class="settings-item">
<label for="timeoutInput">Session Timeout</label>
<div class="timeout-input-group">
<input type="number" id="timeoutInput" min="3" max="300" value="15">
<span>seconds</span>
</div>
</div>
</div>
</div>
<button id="themeToggle" class="btn-icon-only" title="Switch to dark mode"> <button id="themeToggle" class="btn-icon-only" title="Switch to dark mode">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
@ -84,6 +73,14 @@
</div> </div>
<div class="requests-container" id="requestsContainer"> <div class="requests-container" id="requestsContainer">
<div class="loading-requests hidden" id="loadingRequestsMessage">
<div class="loading-state">
<div class="loading-spinner"></div>
<p>Loading requests...</p>
<small>Fetching pending signature requests</small>
</div>
</div>
<div class="no-requests" id="noRequestsMessage"> <div class="no-requests" id="noRequestsMessage">
<div class="empty-state"> <div class="empty-state">
<div class="empty-icon">📝</div> <div class="empty-icon">📝</div>
@ -106,14 +103,6 @@
</svg> </svg>
Refresh Refresh
</button> </button>
<button id="sigSocketStatusBtn" class="btn btn-ghost btn-small">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="6" x2="12" y2="12"></line>
<line x1="16" y1="16" x2="12" y2="12"></line>
</svg>
Status
</button>
</div> </div>
</div> </div>
@ -235,8 +224,41 @@
</div> </div>
</section> </section>
<!-- Settings Section -->
<section class="section hidden" id="settingsSection">
<div class="settings-header">
<h2>Settings</h2>
</div>
<!-- Session Settings -->
<div class="card">
<h3>Session Settings</h3>
<div class="settings-item">
<label for="timeoutInput">Session Timeout</label>
<div class="timeout-input-group">
<input type="number" id="timeoutInput" min="3" max="300" value="15">
<span>seconds</span>
</div>
<small class="settings-help">Automatically lock session after inactivity</small>
</div>
</div>
<!-- SigSocket Settings -->
<div class="card">
<h3>SigSocket Settings</h3>
<div class="settings-item">
<label for="serverUrlInput">Server URL</label>
<div class="server-input-group">
<input type="text" id="serverUrlInput" placeholder="ws://localhost:8080/ws" value="ws://localhost:8080/ws">
<button id="saveServerUrlBtn" class="btn btn-small btn-primary">Save</button>
</div>
<small class="settings-help">WebSocket URL for SigSocket server (ws:// or wss://)</small>
</div>
</div>
</section>
</div> </div>

View File

@ -32,6 +32,11 @@ function showToast(message, type = 'info') {
// Enhanced loading states for buttons // Enhanced loading states for buttons
function setButtonLoading(button, loading = true) { function setButtonLoading(button, loading = true) {
// Handle null/undefined button gracefully
if (!button) {
return;
}
if (loading) { if (loading) {
button.dataset.originalText = button.textContent; button.dataset.originalText = button.textContent;
button.classList.add('loading'); button.classList.add('loading');
@ -126,9 +131,18 @@ const elements = {
// Header elements // Header elements
lockBtn: document.getElementById('lockBtn'), lockBtn: document.getElementById('lockBtn'),
themeToggle: document.getElementById('themeToggle'), themeToggle: document.getElementById('themeToggle'),
settingsToggle: document.getElementById('settingsToggle'), settingsBtn: document.getElementById('settingsBtn'),
settingsDropdown: document.getElementById('settingsDropdown'), headerTitle: document.getElementById('headerTitle'),
// Section elements
authSection: document.getElementById('authSection'),
vaultSection: document.getElementById('vaultSection'),
settingsSection: document.getElementById('settingsSection'),
// Settings page elements
timeoutInput: document.getElementById('timeoutInput'), timeoutInput: document.getElementById('timeoutInput'),
serverUrlInput: document.getElementById('serverUrlInput'),
saveServerUrlBtn: document.getElementById('saveServerUrlBtn'),
// Keypair management elements // Keypair management elements
toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'), toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'),
@ -219,6 +233,53 @@ async function saveTimeoutSetting(timeout) {
await sendMessage('updateTimeout', { timeout }); await sendMessage('updateTimeout', { timeout });
} }
// Server URL settings
async function loadServerUrlSetting() {
try {
const result = await chrome.storage.local.get(['sigSocketUrl']);
const serverUrl = result.sigSocketUrl || 'ws://localhost:8080/ws';
if (elements.serverUrlInput) {
elements.serverUrlInput.value = serverUrl;
}
} catch (error) {
console.warn('Failed to load server URL setting:', error);
}
}
async function saveServerUrlSetting() {
try {
const serverUrl = elements.serverUrlInput?.value?.trim();
if (!serverUrl) {
showToast('Please enter a valid server URL', 'error');
return;
}
// Basic URL validation
if (!serverUrl.startsWith('ws://') && !serverUrl.startsWith('wss://')) {
showToast('Server URL must start with ws:// or wss://', 'error');
return;
}
// Save to storage
await chrome.storage.local.set({ sigSocketUrl: serverUrl });
// Notify background script to update server URL
const response = await sendMessage('updateSigSocketUrl', { serverUrl });
if (response?.success) {
showToast('Server URL saved successfully', 'success');
// Refresh connection status
await loadSigSocketState();
} else {
showToast('Failed to update server URL', 'error');
}
} catch (error) {
console.error('Failed to save server URL:', error);
showToast('Failed to save server URL', 'error');
}
}
async function resetSessionTimeout() { async function resetSessionTimeout() {
if (currentKeyspace) { if (currentKeyspace) {
await sendMessage('resetTimeout'); await sendMessage('resetTimeout');
@ -241,11 +302,63 @@ function toggleTheme() {
updateThemeIcon(newTheme); updateThemeIcon(newTheme);
} }
// Settings dropdown management // Settings page navigation
function toggleSettingsDropdown() { async function showSettingsPage() {
const dropdown = elements.settingsDropdown; // Hide all sections
if (dropdown) { document.querySelectorAll('.section').forEach(section => {
dropdown.classList.toggle('hidden'); section.classList.add('hidden');
});
// Show settings section
elements.settingsSection?.classList.remove('hidden');
// Ensure we have current status before updating settings display
await loadSigSocketState();
}
async function hideSettingsPage() {
// Hide settings section
elements.settingsSection?.classList.add('hidden');
// Check current session state to determine what to show
try {
const response = await sendMessage('getStatus');
if (response && response.success && response.status && response.session) {
// Active session exists - show vault section
currentKeyspace = response.session.keyspace;
if (elements.keyspaceInput) {
elements.keyspaceInput.value = currentKeyspace;
}
setStatus(currentKeyspace, true);
elements.vaultSection?.classList.remove('hidden');
updateSettingsVisibility(); // Update settings visibility
// Load vault content
await loadKeypairs();
// Use retry mechanism for existing sessions that might have stale connections
await loadSigSocketStateWithRetry();
} else {
// No active session - show auth section
currentKeyspace = null;
setStatus('', false);
elements.authSection?.classList.remove('hidden');
updateSettingsVisibility(); // Update settings visibility
// For no session, use regular loading
await loadSigSocketState();
}
} catch (error) {
console.warn('Failed to check session state:', error);
// Fallback to auth section on error
currentKeyspace = null;
setStatus('', false);
elements.authSection?.classList.remove('hidden');
updateSettingsVisibility(); // Update settings visibility
// Still try to load SigSocket state
await loadSigSocketState();
} }
} }
@ -287,6 +400,19 @@ function updateThemeIcon(theme) {
} }
} }
// Update settings button visibility based on keyspace state
function updateSettingsVisibility() {
if (elements.settingsBtn) {
if (currentKeyspace) {
// Show settings when keyspace is unlocked
elements.settingsBtn.style.display = '';
} else {
// Hide settings when keyspace is locked
elements.settingsBtn.style.display = 'none';
}
}
}
// Establish connection to background script for keep-alive // Establish connection to background script for keep-alive
function connectToBackground() { function connectToBackground() {
backgroundPort = chrome.runtime.connect({ name: 'popup' }); backgroundPort = chrome.runtime.connect({ name: 'popup' });
@ -299,6 +425,7 @@ function connectToBackground() {
selectedKeypairId = null; selectedKeypairId = null;
setStatus('', false); setStatus('', false);
showSection('authSection'); showSection('authSection');
updateSettingsVisibility(); // Update settings visibility
clearVaultState(); clearVaultState();
// Clear form inputs // Clear form inputs
@ -313,6 +440,13 @@ function connectToBackground() {
backgroundPort.onDisconnect.addListener(() => { backgroundPort.onDisconnect.addListener(() => {
backgroundPort = null; backgroundPort = null;
}); });
// Immediately request status update when popup connects
setTimeout(() => {
if (backgroundPort) {
backgroundPort.postMessage({ type: 'REQUEST_IMMEDIATE_STATUS' });
}
}, 50); // Small delay to ensure connection is established
} }
// Initialize // Initialize
@ -323,6 +457,9 @@ document.addEventListener('DOMContentLoaded', async function() {
// Load timeout setting // Load timeout setting
await loadTimeoutSetting(); await loadTimeoutSetting();
// Load server URL setting
await loadServerUrlSetting();
// Ensure lock button starts hidden // Ensure lock button starts hidden
const lockBtn = document.getElementById('lockBtn'); const lockBtn = document.getElementById('lockBtn');
if (lockBtn) { if (lockBtn) {
@ -338,7 +475,9 @@ document.addEventListener('DOMContentLoaded', async function() {
loginBtn: login, loginBtn: login,
lockBtn: lockSession, lockBtn: lockSession,
themeToggle: toggleTheme, themeToggle: toggleTheme,
settingsToggle: toggleSettingsDropdown, settingsBtn: showSettingsPage,
headerTitle: hideSettingsPage,
saveServerUrlBtn: saveServerUrlSetting,
toggleAddKeypairBtn: toggleAddKeypairForm, toggleAddKeypairBtn: toggleAddKeypairForm,
addKeypairBtn: addKeypair, addKeypairBtn: addKeypair,
cancelAddKeypairBtn: hideAddKeypairForm, cancelAddKeypairBtn: hideAddKeypairForm,
@ -349,7 +488,10 @@ document.addEventListener('DOMContentLoaded', async function() {
}; };
Object.entries(eventMap).forEach(([elementKey, handler]) => { Object.entries(eventMap).forEach(([elementKey, handler]) => {
elements[elementKey]?.addEventListener('click', handler); const element = elements[elementKey];
if (element) {
element.addEventListener('click', handler);
}
}); });
// Tab functionality // Tab functionality
@ -400,10 +542,66 @@ document.addEventListener('DOMContentLoaded', async function() {
} }
}); });
// Initialize SigSocket UI elements after DOM is ready
sigSocketElements = {
connectionStatus: document.getElementById('connectionStatus'),
connectionDot: document.getElementById('connectionDot'),
connectionText: document.getElementById('connectionText'),
requestsContainer: document.getElementById('requestsContainer'),
loadingRequestsMessage: document.getElementById('loadingRequestsMessage'),
noRequestsMessage: document.getElementById('noRequestsMessage'),
requestsList: document.getElementById('requestsList'),
refreshRequestsBtn: document.getElementById('refreshRequestsBtn')
};
// Add SigSocket button listeners
sigSocketElements.refreshRequestsBtn?.addEventListener('click', refreshSigSocketRequests);
// Check if opened via notification (focus on SigSocket section)
const urlParams = new URLSearchParams(window.location.search);
const fromNotification = urlParams.get('from') === 'notification';
// Check for existing session // Check for existing session
await checkExistingSession(); await checkExistingSession();
// If opened from notification, focus on SigSocket section and show requests
if (fromNotification) {
console.log('🔔 Opened from notification, focusing on SigSocket section');
focusOnSigSocketSection();
}
// Try to load any cached SigSocket state immediately for better UX
await loadCachedSigSocketState();
}); });
// Focus on SigSocket section when opened from notification
function focusOnSigSocketSection() {
try {
// Switch to SigSocket tab if not already active
const sigSocketTab = document.querySelector('[data-tab="sigsocket"]');
if (sigSocketTab && !sigSocketTab.classList.contains('active')) {
sigSocketTab.click();
}
// Scroll to requests container
if (sigSocketElements.requestsContainer) {
sigSocketElements.requestsContainer.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
// Show a helpful toast
showToast('New signature request received! Review pending requests below.', 'info');
// Refresh requests to ensure latest state
setTimeout(() => refreshSigSocketRequests(), 500);
} catch (error) {
console.error('Failed to focus on SigSocket section:', error);
}
}
async function checkExistingSession() { async function checkExistingSession() {
try { try {
const response = await sendMessage('getStatus'); const response = await sendMessage('getStatus');
@ -413,15 +611,31 @@ async function checkExistingSession() {
elements.keyspaceInput.value = currentKeyspace; elements.keyspaceInput.value = currentKeyspace;
setStatus(currentKeyspace, true); setStatus(currentKeyspace, true);
showSection('vaultSection'); showSection('vaultSection');
updateSettingsVisibility(); // Update settings visibility
await loadKeypairs(); await loadKeypairs();
// Use retry mechanism for existing sessions to handle stale connections
await loadSigSocketStateWithRetry();
} else { } else {
// No active session // No active session
currentKeyspace = null;
setStatus('', false); setStatus('', false);
showSection('authSection'); showSection('authSection');
updateSettingsVisibility(); // Update settings visibility
// For no session, use regular loading (no retry needed)
await loadSigSocketState();
} }
} catch (error) { } catch (error) {
setStatus('', false); setStatus('', false);
showSection('authSection'); showSection('authSection');
// Still try to load SigSocket state even on error
try {
await loadSigSocketState();
} catch (sigSocketError) {
console.warn('Failed to load SigSocket state:', sigSocketError);
}
} }
} }
@ -641,9 +855,23 @@ async function login() {
currentKeyspace = auth.keyspace; currentKeyspace = auth.keyspace;
setStatus(auth.keyspace, true); setStatus(auth.keyspace, true);
showSection('vaultSection'); showSection('vaultSection');
updateSettingsVisibility(); // Update settings visibility
clearVaultState(); clearVaultState();
await loadKeypairs(); await loadKeypairs();
// Clean flow: Login -> Connect -> Restore -> Display
console.log('🔓 Login successful, applying clean flow...');
// 1. Wait for SigSocket to connect and restore requests
await loadSigSocketStateWithRetry();
// 2. Show loading state while fetching
showRequestsLoading();
// 3. Refresh requests to get the clean, restored state
await refreshSigSocketRequests();
console.log('✅ Login clean flow completed');
return response; return response;
} else { } else {
throw new Error(getResponseError(response, 'login')); throw new Error(getResponseError(response, 'login'));
@ -667,6 +895,7 @@ async function lockSession() {
selectedKeypairId = null; selectedKeypairId = null;
setStatus('', false); setStatus('', false);
showSection('authSection'); showSection('authSection');
updateSettingsVisibility(); // Update settings visibility
// Clear all form inputs // Clear all form inputs
elements.keyspaceInput.value = ''; elements.keyspaceInput.value = '';
@ -936,28 +1165,8 @@ const verifySignature = () => performCryptoOperation({
// SigSocket functionality // SigSocket functionality
let sigSocketRequests = []; let sigSocketRequests = [];
let sigSocketStatus = { isConnected: false, workspace: null }; let sigSocketStatus = { isConnected: false, workspace: null };
let sigSocketElements = {}; // Will be initialized in DOMContentLoaded
// Initialize SigSocket UI elements let isInitialLoad = true; // Track if this is the first load
const sigSocketElements = {
connectionStatus: document.getElementById('connectionStatus'),
connectionDot: document.getElementById('connectionDot'),
connectionText: document.getElementById('connectionText'),
requestsContainer: document.getElementById('requestsContainer'),
noRequestsMessage: document.getElementById('noRequestsMessage'),
requestsList: document.getElementById('requestsList'),
refreshRequestsBtn: document.getElementById('refreshRequestsBtn'),
sigSocketStatusBtn: document.getElementById('sigSocketStatusBtn')
};
// Add SigSocket event listeners
document.addEventListener('DOMContentLoaded', () => {
// Add SigSocket button listeners
sigSocketElements.refreshRequestsBtn?.addEventListener('click', refreshSigSocketRequests);
sigSocketElements.sigSocketStatusBtn?.addEventListener('click', showSigSocketStatus);
// Load initial SigSocket state
loadSigSocketState();
});
// Listen for messages from background script about SigSocket events // Listen for messages from background script about SigSocket events
if (backgroundPort) { if (backgroundPort) {
@ -968,6 +1177,12 @@ if (backgroundPort) {
updateRequestsList(message.pendingRequests); updateRequestsList(message.pendingRequests);
} else if (message.type === 'KEYSPACE_UNLOCKED') { } else if (message.type === 'KEYSPACE_UNLOCKED') {
handleKeypaceUnlocked(message); handleKeypaceUnlocked(message);
} else if (message.type === 'CONNECTION_STATUS_CHANGED') {
handleConnectionStatusChanged(message);
} else if (message.type === 'FOCUS_SIGSOCKET') {
// Handle focus request from notification click
console.log('🔔 Received focus request from notification');
focusOnSigSocketSection();
} }
}); });
} }
@ -975,19 +1190,117 @@ if (backgroundPort) {
// Load SigSocket state when popup opens // Load SigSocket state when popup opens
async function loadSigSocketState() { async function loadSigSocketState() {
try { try {
// Get SigSocket status console.log('🔄 Loading SigSocket state...');
const statusResponse = await sendMessage('getSigSocketStatus');
if (statusResponse?.success) { // Show loading state for requests
updateConnectionStatus(statusResponse.status); showRequestsLoading();
// Show loading state for connection status on initial load
if (isInitialLoad) {
showConnectionLoading();
} }
// Get pending requests // Force a fresh connection status check with enhanced testing
const statusResponse = await sendMessage('getSigSocketStatusWithTest');
if (statusResponse?.success) {
console.log('✅ Got SigSocket status:', statusResponse.status);
updateConnectionStatus(statusResponse.status);
} else {
console.warn('Enhanced status check failed, trying fallback...');
// Fallback to regular status check
const fallbackResponse = await sendMessage('getSigSocketStatus');
if (fallbackResponse?.success) {
console.log('✅ Got fallback SigSocket status:', fallbackResponse.status);
updateConnectionStatus(fallbackResponse.status);
} else {
// If both fail, show disconnected but don't show error on initial load
updateConnectionStatus({
isConnected: false,
workspace: null,
publicKey: null,
pendingRequestCount: 0,
serverUrl: 'ws://localhost:8080/ws'
});
}
}
// Get pending requests - this now works even when keyspace is locked
console.log('📋 Fetching pending requests...');
const requestsResponse = await sendMessage('getPendingSignRequests'); const requestsResponse = await sendMessage('getPendingSignRequests');
if (requestsResponse?.success) { if (requestsResponse?.success) {
console.log(`📋 Retrieved ${requestsResponse.requests?.length || 0} pending requests:`, requestsResponse.requests);
updateRequestsList(requestsResponse.requests); updateRequestsList(requestsResponse.requests);
} else {
console.warn('Failed to get pending requests:', requestsResponse);
updateRequestsList([]);
} }
// Mark initial load as complete
isInitialLoad = false;
} catch (error) { } catch (error) {
console.warn('Failed to load SigSocket state:', error); console.warn('Failed to load SigSocket state:', error);
// Hide loading state and show error state
hideRequestsLoading();
// Set disconnected state on error (but don't show error toast on initial load)
updateConnectionStatus({
isConnected: false,
workspace: null,
publicKey: null,
pendingRequestCount: 0,
serverUrl: 'ws://localhost:8080/ws'
});
// Still try to show any cached requests
updateRequestsList([]);
// Mark initial load as complete
isInitialLoad = false;
}
}
// Load cached SigSocket state for immediate display
async function loadCachedSigSocketState() {
try {
// Try to get any cached requests from storage for immediate display
const cachedData = await chrome.storage.local.get(['sigSocketPendingRequests']);
if (cachedData.sigSocketPendingRequests && Array.isArray(cachedData.sigSocketPendingRequests)) {
console.log('📋 Loading cached requests for immediate display');
updateRequestsList(cachedData.sigSocketPendingRequests);
}
} catch (error) {
console.warn('Failed to load cached SigSocket state:', error);
}
}
// Load SigSocket state with simple retry for session initialization timing
async function loadSigSocketStateWithRetry() {
// First try immediately (might already be connected)
await loadSigSocketState();
// If still showing disconnected after initial load, try again after a short delay
if (!sigSocketStatus.isConnected) {
console.log('🔄 Initial load showed disconnected, retrying after delay...');
await new Promise(resolve => setTimeout(resolve, 500));
await loadSigSocketState();
}
}
// Show loading state for connection status
function showConnectionLoading() {
if (sigSocketElements.connectionDot && sigSocketElements.connectionText) {
sigSocketElements.connectionDot.classList.remove('connected');
sigSocketElements.connectionDot.classList.add('loading');
sigSocketElements.connectionText.textContent = 'Checking...';
}
}
// Hide loading state for connection status
function hideConnectionLoading() {
if (sigSocketElements.connectionDot) {
sigSocketElements.connectionDot.classList.remove('loading');
} }
} }
@ -995,15 +1308,42 @@ async function loadSigSocketState() {
function updateConnectionStatus(status) { function updateConnectionStatus(status) {
sigSocketStatus = status; sigSocketStatus = status;
// Hide loading state
hideConnectionLoading();
if (sigSocketElements.connectionDot && sigSocketElements.connectionText) { if (sigSocketElements.connectionDot && sigSocketElements.connectionText) {
if (status.isConnected) { if (status.isConnected) {
sigSocketElements.connectionDot.classList.add('connected'); sigSocketElements.connectionDot.classList.add('connected');
sigSocketElements.connectionText.textContent = `Connected (${status.workspace || 'Unknown'})`; sigSocketElements.connectionText.textContent = 'Connected';
} else { } else {
sigSocketElements.connectionDot.classList.remove('connected'); sigSocketElements.connectionDot.classList.remove('connected');
sigSocketElements.connectionText.textContent = 'Disconnected'; sigSocketElements.connectionText.textContent = 'Disconnected';
} }
} }
// Log connection details for debugging
console.log('🔗 Connection status updated:', {
connected: status.isConnected,
workspace: status.workspace,
publicKey: status.publicKey?.substring(0, 16) + '...',
serverUrl: status.serverUrl
});
}
// Show loading state for requests
function showRequestsLoading() {
if (!sigSocketElements.requestsContainer) return;
sigSocketElements.loadingRequestsMessage?.classList.remove('hidden');
sigSocketElements.noRequestsMessage?.classList.add('hidden');
sigSocketElements.requestsList?.classList.add('hidden');
}
// Hide loading state for requests
function hideRequestsLoading() {
if (!sigSocketElements.requestsContainer) return;
sigSocketElements.loadingRequestsMessage?.classList.add('hidden');
} }
// Update requests list display // Update requests list display
@ -1012,6 +1352,9 @@ function updateRequestsList(requests) {
if (!sigSocketElements.requestsContainer) return; if (!sigSocketElements.requestsContainer) return;
// Hide loading state
hideRequestsLoading();
if (sigSocketRequests.length === 0) { if (sigSocketRequests.length === 0) {
sigSocketElements.noRequestsMessage?.classList.remove('hidden'); sigSocketElements.noRequestsMessage?.classList.remove('hidden');
sigSocketElements.requestsList?.classList.add('hidden'); sigSocketElements.requestsList?.classList.add('hidden');
@ -1036,8 +1379,42 @@ function createRequestItem(request) {
const shortId = request.id.substring(0, 8) + '...'; const shortId = request.id.substring(0, 8) + '...';
const decodedMessage = request.message ? atob(request.message) : 'No message'; const decodedMessage = request.message ? atob(request.message) : 'No message';
// Check if keyspace is currently unlocked
const isKeypaceUnlocked = currentKeyspace !== null;
// Create different UI based on keyspace lock status
let actionsHtml;
let statusIndicator = '';
if (isKeypaceUnlocked) {
// Normal approve/reject buttons when unlocked
actionsHtml = `
<div class="request-actions">
<button class="btn-approve" data-request-id="${request.id}">
Approve
</button>
<button class="btn-reject" data-request-id="${request.id}">
Reject
</button>
</div>
`;
} else {
// Show pending status and unlock message when locked
statusIndicator = '<div class="request-status pending">⏳ Pending - Unlock keyspace to approve/reject</div>';
actionsHtml = `
<div class="request-actions locked">
<button class="btn-approve" data-request-id="${request.id}" disabled title="Unlock keyspace to approve">
Approve
</button>
<button class="btn-reject" data-request-id="${request.id}" disabled title="Unlock keyspace to reject">
Reject
</button>
</div>
`;
}
return ` return `
<div class="request-item" data-request-id="${request.id}"> <div class="request-item ${isKeypaceUnlocked ? '' : 'locked'}" data-request-id="${request.id}">
<div class="request-header"> <div class="request-header">
<div class="request-id" title="${request.id}">${shortId}</div> <div class="request-id" title="${request.id}">${shortId}</div>
<div class="request-time">${requestTime}</div> <div class="request-time">${requestTime}</div>
@ -1047,14 +1424,8 @@ function createRequestItem(request) {
${decodedMessage.length > 100 ? decodedMessage.substring(0, 100) + '...' : decodedMessage} ${decodedMessage.length > 100 ? decodedMessage.substring(0, 100) + '...' : decodedMessage}
</div> </div>
<div class="request-actions"> ${statusIndicator}
<button class="btn-approve" data-request-id="${request.id}"> ${actionsHtml}
Approve
</button>
<button class="btn-reject" data-request-id="${request.id}">
Reject
</button>
</div>
</div> </div>
`; `;
} }
@ -1091,15 +1462,61 @@ function handleNewSignRequest(message) {
} }
} }
// Handle keyspace unlocked event // Handle keyspace unlocked event - Clean flow implementation
function handleKeypaceUnlocked(message) { function handleKeypaceUnlocked(message) {
// Update requests list console.log('🔓 Keyspace unlocked - applying clean flow for request display');
if (message.pendingRequests) {
updateRequestsList(message.pendingRequests);
}
// Update button states based on whether requests can be approved // Clean flow: Unlock -> Show loading -> Display requests -> Update UI
updateRequestButtonStates(message.canApprove); try {
// 1. Show loading state immediately
showRequestsLoading();
// 2. Update requests list with restored + current requests
if (message.pendingRequests && Array.isArray(message.pendingRequests)) {
console.log(`📋 Displaying ${message.pendingRequests.length} restored requests`);
updateRequestsList(message.pendingRequests);
// 3. Update button states (should be enabled now)
updateRequestButtonStates(message.canApprove !== false);
// 4. Show appropriate notification
const count = message.pendingRequests.length;
if (count > 0) {
showToast(`Keyspace unlocked! ${count} pending request${count > 1 ? 's' : ''} ready for review.`, 'info');
} else {
showToast('Keyspace unlocked! No pending requests.', 'success');
}
} else {
// 5. If no requests in message, fetch fresh from server
console.log('📋 No requests in unlock message, fetching from server...');
setTimeout(() => refreshSigSocketRequests(), 100);
}
console.log('✅ Keyspace unlock flow completed');
} catch (error) {
console.error('❌ Error in keyspace unlock flow:', error);
hideRequestsLoading();
showToast('Error loading requests after unlock', 'error');
}
}
// Handle connection status change event
function handleConnectionStatusChanged(message) {
console.log('🔄 Connection status changed:', message.status);
// Store previous state for comparison
const previousState = sigSocketStatus ? sigSocketStatus.isConnected : null;
// Update the connection status display
updateConnectionStatus(message.status);
// Only show toast for actual changes, not initial status, and not during initial load
if (!isInitialLoad && previousState !== null && previousState !== message.status.isConnected) {
const statusText = message.status.isConnected ? 'Connected' : 'Disconnected';
const toastType = message.status.isConnected ? 'success' : 'warning';
showToast(`SigSocket ${statusText}`, toastType);
}
} }
// Show workspace mismatch warning // Show workspace mismatch warning
@ -1133,30 +1550,46 @@ function updateRequestButtonStates(canApprove) {
// Approve a sign request // Approve a sign request
async function approveSignRequest(requestId) { async function approveSignRequest(requestId) {
let button = null;
try { try {
const button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`);
setButtonLoading(button, true); setButtonLoading(button, true);
const response = await sendMessage('approveSignRequest', { requestId }); const response = await sendMessage('approveSignRequest', { requestId });
if (response?.success) { if (response?.success) {
showToast('Request approved and signed!', 'success'); showToast('Request approved and signed!', 'success');
showRequestsLoading();
await refreshSigSocketRequests(); await refreshSigSocketRequests();
} else { } else {
throw new Error(getResponseError(response, 'approve request')); const errorMsg = getResponseError(response, 'approve request');
// Check for specific connection errors
if (errorMsg.includes('Connection not found') || errorMsg.includes('public key')) {
showToast('Connection error: Please check SigSocket connection and try again', 'error');
// Trigger a connection status refresh
await loadSigSocketState();
} else if (errorMsg.includes('keyspace') || errorMsg.includes('locked')) {
showToast('Keyspace is locked. Please unlock to approve requests.', 'error');
} else {
throw new Error(errorMsg);
}
} }
} catch (error) { } catch (error) {
console.error('Error approving request:', error);
showToast(`Failed to approve request: ${error.message}`, 'error'); showToast(`Failed to approve request: ${error.message}`, 'error');
} finally { } finally {
const button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`); // Re-query button in case DOM was updated during the operation
setButtonLoading(button, false); const finalButton = document.querySelector(`[data-request-id="${requestId}"].btn-approve`);
setButtonLoading(finalButton, false);
} }
} }
// Reject a sign request // Reject a sign request
async function rejectSignRequest(requestId) { async function rejectSignRequest(requestId) {
let button = null;
try { try {
const button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`);
setButtonLoading(button, true); setButtonLoading(button, true);
const response = await sendMessage('rejectSignRequest', { const response = await sendMessage('rejectSignRequest', {
@ -1166,6 +1599,7 @@ async function rejectSignRequest(requestId) {
if (response?.success) { if (response?.success) {
showToast('Request rejected', 'info'); showToast('Request rejected', 'info');
showRequestsLoading();
await refreshSigSocketRequests(); await refreshSigSocketRequests();
} else { } else {
throw new Error(getResponseError(response, 'reject request')); throw new Error(getResponseError(response, 'reject request'));
@ -1173,8 +1607,9 @@ async function rejectSignRequest(requestId) {
} catch (error) { } catch (error) {
showToast(`Failed to reject request: ${error.message}`, 'error'); showToast(`Failed to reject request: ${error.message}`, 'error');
} finally { } finally {
const button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`); // Re-query button in case DOM was updated during the operation
setButtonLoading(button, false); const finalButton = document.querySelector(`[data-request-id="${requestId}"].btn-reject`);
setButtonLoading(finalButton, false);
} }
} }
@ -1182,42 +1617,35 @@ async function rejectSignRequest(requestId) {
async function refreshSigSocketRequests() { async function refreshSigSocketRequests() {
try { try {
setButtonLoading(sigSocketElements.refreshRequestsBtn, true); setButtonLoading(sigSocketElements.refreshRequestsBtn, true);
showRequestsLoading();
console.log('🔄 Refreshing SigSocket requests...');
const response = await sendMessage('getPendingSignRequests'); const response = await sendMessage('getPendingSignRequests');
if (response?.success) { if (response?.success) {
console.log(`📋 Retrieved ${response.requests?.length || 0} pending requests`);
updateRequestsList(response.requests); updateRequestsList(response.requests);
showToast('Requests refreshed', 'success');
const count = response.requests?.length || 0;
if (count > 0) {
showToast(`${count} pending request${count > 1 ? 's' : ''} found`, 'success');
} else {
showToast('No pending requests', 'info');
}
} else { } else {
console.error('Failed to get pending requests:', response);
hideRequestsLoading();
throw new Error(getResponseError(response, 'refresh requests')); throw new Error(getResponseError(response, 'refresh requests'));
} }
} catch (error) { } catch (error) {
console.error('Error refreshing requests:', error);
hideRequestsLoading();
showToast(`Failed to refresh requests: ${error.message}`, 'error'); showToast(`Failed to refresh requests: ${error.message}`, 'error');
} finally { } finally {
setButtonLoading(sigSocketElements.refreshRequestsBtn, false); setButtonLoading(sigSocketElements.refreshRequestsBtn, false);
} }
} }
// Show SigSocket status
async function showSigSocketStatus() {
try {
const response = await sendMessage('getSigSocketStatus');
if (response?.success) {
const status = response.status;
const statusText = `
SigSocket Status:
Connected: ${status.isConnected ? 'Yes' : 'No'}
Workspace: ${status.workspace || 'None'}
Public Key: ${status.publicKey ? status.publicKey.substring(0, 16) + '...' : 'None'}
Pending Requests: ${status.pendingRequestCount || 0}
Server URL: ${status.serverUrl}
`.trim();
showToast(statusText, 'info');
updateConnectionStatus(status);
} else {
throw new Error(getResponseError(response, 'get status'));
}
} catch (error) {
showToast(`Failed to get status: ${error.message}`, 'error');
}
}

View File

@ -188,6 +188,15 @@ body {
margin: 0; margin: 0;
} }
.clickable-header {
cursor: pointer;
transition: opacity 0.2s ease;
}
.clickable-header:hover {
opacity: 0.8;
}
.header-actions { .header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
@ -261,6 +270,75 @@ body {
color: var(--text-muted); color: var(--text-muted);
} }
.server-input-group {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.server-input-group input {
flex: 1;
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 14px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-input);
color: var(--text-primary);
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
.server-input-group input:focus {
outline: none;
border-color: var(--border-focus);
box-shadow: 0 0 0 2px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.15);
}
.settings-help {
display: block;
font-size: 12px;
color: var(--text-muted);
margin-top: var(--spacing-xs);
font-style: italic;
}
/* Settings page styles */
.settings-header {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.settings-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
}
.about-info {
text-align: left;
}
.about-info p {
margin: 0 0 var(--spacing-xs) 0;
font-size: 14px;
color: var(--text-primary);
}
.about-info strong {
font-weight: 600;
}
.version-info {
font-size: 12px;
color: var(--text-muted);
font-style: italic;
}
.btn-icon-only { .btn-icon-only {
background: var(--bg-button-ghost); background: var(--bg-button-ghost);
border: none; border: none;
@ -456,6 +534,17 @@ input::placeholder, textarea::placeholder {
font-size: 12px; font-size: 12px;
} }
/* Button icon spacing */
.btn svg {
margin-right: var(--spacing-xs);
flex-shrink: 0;
}
.btn svg:last-child {
margin-right: 0;
margin-left: var(--spacing-xs);
}
.btn:disabled { .btn:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
@ -1073,18 +1162,19 @@ input::placeholder, textarea::placeholder {
/* SigSocket Requests Styles */ /* SigSocket Requests Styles */
.sigsocket-section { .sigsocket-section {
margin-bottom: 20px; margin-bottom: var(--spacing-lg);
} }
.section-header { .section-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 15px; margin-bottom: var(--spacing-lg);
} }
.section-header h3 { .section-header h3 {
margin: 0; margin: 0;
color: var(--text-primary);
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
} }
@ -1092,7 +1182,7 @@ input::placeholder, textarea::placeholder {
.connection-status { .connection-status {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: var(--spacing-xs);
font-size: 12px; font-size: 12px;
color: var(--text-secondary); color: var(--text-secondary);
} }
@ -1109,16 +1199,62 @@ input::placeholder, textarea::placeholder {
background: var(--accent-success); background: var(--accent-success);
} }
.status-dot.loading {
background: var(--accent-warning);
animation: pulse-dot 1.5s ease-in-out infinite;
}
@keyframes pulse-dot {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.2);
}
}
.requests-container { .requests-container {
min-height: 80px; min-height: 80px;
} }
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 20px; padding: var(--spacing-xl);
color: var(--text-secondary); color: var(--text-secondary);
} }
.loading-state {
text-align: center;
padding: var(--spacing-xl);
color: var(--text-secondary);
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border-color);
border-top: 2px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto var(--spacing-sm) auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-state p {
margin: var(--spacing-sm) 0;
font-weight: 500;
}
.loading-state small {
opacity: 0.8;
}
.empty-icon { .empty-icon {
font-size: 24px; font-size: 24px;
margin-bottom: 8px; margin-bottom: 8px;
@ -1228,10 +1364,42 @@ input::placeholder, textarea::placeholder {
.sigsocket-actions { .sigsocket-actions {
display: flex; display: flex;
gap: 8px; gap: var(--spacing-sm);
margin-top: 12px; margin-top: var(--spacing-md);
padding-top: 12px; }
border-top: 1px solid var(--border-color);
/* Ensure refresh button follows design system */
#refreshRequestsBtn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
}
/* Request item locked state styles */
.request-item.locked {
opacity: 0.8;
border-left: 3px solid var(--warning-color, #ffa500);
}
.request-status.pending {
background: var(--warning-bg, #fff3cd);
color: var(--warning-text, #856404);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: 4px;
font-size: 12px;
margin: var(--spacing-xs) 0;
border: 1px solid var(--warning-border, #ffeaa7);
}
.request-actions.locked button {
opacity: 0.6;
cursor: not-allowed;
}
.request-actions.locked button:hover {
background: var(--button-bg) !important;
transform: none !important;
} }
.workspace-mismatch { .workspace-mismatch {

View File

@ -467,31 +467,31 @@ export function run_rhai(script) {
} }
function __wbg_adapter_34(arg0, arg1, arg2) { function __wbg_adapter_34(arg0, arg1, arg2) {
wasm.closure174_externref_shim(arg0, arg1, arg2); wasm.closure203_externref_shim(arg0, arg1, arg2);
} }
function __wbg_adapter_39(arg0, arg1) { function __wbg_adapter_39(arg0, arg1) {
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha4436a3f79fb1a0f(arg0, arg1); wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd79bf9f6d48e92f7(arg0, arg1);
} }
function __wbg_adapter_44(arg0, arg1, arg2) { function __wbg_adapter_44(arg0, arg1, arg2) {
wasm.closure237_externref_shim(arg0, arg1, arg2); wasm.closure239_externref_shim(arg0, arg1, arg2);
} }
function __wbg_adapter_49(arg0, arg1) { function __wbg_adapter_49(arg0, arg1) {
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf148c54a4a246cea(arg0, arg1); wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf103de07b8856532(arg0, arg1);
} }
function __wbg_adapter_52(arg0, arg1, arg2) { function __wbg_adapter_52(arg0, arg1, arg2) {
wasm.closure308_externref_shim(arg0, arg1, arg2); wasm.closure319_externref_shim(arg0, arg1, arg2);
} }
function __wbg_adapter_55(arg0, arg1, arg2) { function __wbg_adapter_55(arg0, arg1, arg2) {
wasm.closure392_externref_shim(arg0, arg1, arg2); wasm.closure395_externref_shim(arg0, arg1, arg2);
} }
function __wbg_adapter_207(arg0, arg1, arg2, arg3) { function __wbg_adapter_207(arg0, arg1, arg2, arg3) {
wasm.closure2046_externref_shim(arg0, arg1, arg2, arg3); wasm.closure2042_externref_shim(arg0, arg1, arg2, arg3);
} }
const __wbindgen_enum_BinaryType = ["blob", "arraybuffer"]; const __wbindgen_enum_BinaryType = ["blob", "arraybuffer"];
@ -1217,40 +1217,40 @@ function __wbg_get_imports() {
const ret = false; const ret = false;
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper1015 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper1036 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 309, __wbg_adapter_52); const ret = makeMutClosure(arg0, arg1, 320, __wbg_adapter_52);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper1320 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper1329 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 393, __wbg_adapter_55); const ret = makeMutClosure(arg0, arg1, 396, __wbg_adapter_55);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper423 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper624 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper424 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper625 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper425 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper626 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_39); const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_39);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper428 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper630 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34); const ret = makeMutClosure(arg0, arg1, 201, __wbg_adapter_34);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper765 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_44);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper766 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper766 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_44); const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_44);
return ret; return ret;
}; };
imports.wbg.__wbindgen_closure_wrapper767 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper768 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_44); const ret = makeMutClosure(arg0, arg1, 240, __wbg_adapter_49);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper770 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_49);
return ret; return ret;
}; };
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) { imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {