summaryrefslogtreecommitdiff
path: root/web/static
diff options
context:
space:
mode:
Diffstat (limited to 'web/static')
-rw-r--r--web/static/js/app.js175
1 files changed, 175 insertions, 0 deletions
diff --git a/web/static/js/app.js b/web/static/js/app.js
index f103ae8..380bb70 100644
--- a/web/static/js/app.js
+++ b/web/static/js/app.js
@@ -265,3 +265,178 @@ function toggleTask(taskId) {
}, { passive: true });
});
})();
+
+// Agent Access Request Notifications
+(function() {
+ let wsConnection = null;
+ let reconnectAttempts = 0;
+ const MAX_RECONNECT_ATTEMPTS = 5;
+ const RECONNECT_DELAY_BASE = 1000;
+
+ function connectWebSocket() {
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const wsUrl = `${protocol}//${window.location.host}/ws/notifications`;
+
+ try {
+ wsConnection = new WebSocket(wsUrl);
+
+ wsConnection.onopen = function() {
+ console.log('WebSocket connected');
+ reconnectAttempts = 0;
+ };
+
+ wsConnection.onmessage = function(event) {
+ try {
+ const msg = JSON.parse(event.data);
+ if (msg.type === 'agent_request') {
+ showAgentApprovalModal(msg.payload);
+ }
+ } catch (e) {
+ console.error('Failed to parse WebSocket message:', e);
+ }
+ };
+
+ wsConnection.onclose = function(event) {
+ console.log('WebSocket disconnected');
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
+ const delay = RECONNECT_DELAY_BASE * Math.pow(2, reconnectAttempts);
+ reconnectAttempts++;
+ console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts})`);
+ setTimeout(connectWebSocket, delay);
+ }
+ };
+
+ wsConnection.onerror = function(error) {
+ console.error('WebSocket error:', error);
+ };
+ } catch (e) {
+ console.error('Failed to create WebSocket:', e);
+ }
+ }
+
+ function showAgentApprovalModal(payload) {
+ // Remove any existing modal
+ const existingModal = document.getElementById('agent-approval-modal');
+ if (existingModal) {
+ existingModal.remove();
+ }
+
+ // Truncate agent ID for display
+ const shortAgentId = payload.agent_id.substring(0, 8);
+
+ // Determine trust indicator
+ let trustBadge = '';
+ let trustClass = '';
+ switch (payload.trust_level) {
+ case 'recognized':
+ trustBadge = 'Recognized';
+ trustClass = 'bg-green-500';
+ break;
+ case 'suspicious':
+ trustBadge = 'Warning: Different ID';
+ trustClass = 'bg-yellow-500';
+ break;
+ default:
+ trustBadge = 'New Agent';
+ trustClass = 'bg-blue-500';
+ }
+
+ // Calculate time remaining
+ const expiresAt = new Date(payload.expires_at);
+ const timeRemaining = Math.max(0, Math.floor((expiresAt - new Date()) / 1000));
+
+ const modal = document.createElement('div');
+ modal.id = 'agent-approval-modal';
+ modal.className = 'fixed inset-0 bg-black/50 flex items-center justify-center z-50';
+ modal.innerHTML = `
+ <div class="bg-gray-900 rounded-lg p-6 max-w-md mx-4 shadow-xl border border-gray-700">
+ <div class="flex items-center justify-between mb-4">
+ <h2 class="text-xl font-semibold text-white">Agent Access Request</h2>
+ <span class="px-2 py-1 text-xs rounded ${trustClass} text-white">${trustBadge}</span>
+ </div>
+ <div class="mb-6 text-gray-300">
+ <p class="mb-2"><strong class="text-white">Agent Name:</strong> ${escapeHtml(payload.agent_name)}</p>
+ <p class="mb-2"><strong class="text-white">Agent ID:</strong> <code class="bg-gray-800 px-1 rounded">${shortAgentId}...</code></p>
+ <p class="text-sm text-gray-400">Expires in <span id="agent-countdown">${timeRemaining}</span>s</p>
+ </div>
+ <div class="flex gap-3">
+ <button id="agent-approve-btn" class="flex-1 bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded transition">
+ Approve
+ </button>
+ <button id="agent-deny-btn" class="flex-1 bg-red-600 hover:bg-red-700 text-white py-2 px-4 rounded transition">
+ Deny
+ </button>
+ </div>
+ </div>
+ `;
+
+ document.body.appendChild(modal);
+
+ // Countdown timer
+ const countdownEl = document.getElementById('agent-countdown');
+ const countdownInterval = setInterval(() => {
+ const remaining = Math.max(0, Math.floor((expiresAt - new Date()) / 1000));
+ countdownEl.textContent = remaining;
+ if (remaining <= 0) {
+ clearInterval(countdownInterval);
+ modal.remove();
+ }
+ }, 1000);
+
+ // Button handlers
+ document.getElementById('agent-approve-btn').addEventListener('click', async () => {
+ await handleAgentDecision(payload.request_token, 'approve');
+ clearInterval(countdownInterval);
+ modal.remove();
+ });
+
+ document.getElementById('agent-deny-btn').addEventListener('click', async () => {
+ await handleAgentDecision(payload.request_token, 'deny');
+ clearInterval(countdownInterval);
+ modal.remove();
+ });
+
+ // Click outside to dismiss (treat as no action)
+ modal.addEventListener('click', (e) => {
+ if (e.target === modal) {
+ clearInterval(countdownInterval);
+ modal.remove();
+ }
+ });
+ }
+
+ async function handleAgentDecision(requestToken, decision) {
+ try {
+ const response = await fetch(`/agent/auth/${decision}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-CSRF-Token': getCSRFToken()
+ },
+ body: JSON.stringify({ request_token: requestToken })
+ });
+
+ if (!response.ok) {
+ const error = await response.text();
+ console.error(`Failed to ${decision} agent:`, error);
+ alert(`Failed to ${decision} agent request. Please try again.`);
+ } else {
+ console.log(`Agent request ${decision}d successfully`);
+ }
+ } catch (e) {
+ console.error(`Error during agent ${decision}:`, e);
+ alert(`Error processing request. Please try again.`);
+ }
+ }
+
+ function escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
+ // Initialize WebSocket on page load
+ document.addEventListener('DOMContentLoaded', function() {
+ connectWebSocket();
+ });
+})();