diff options
Diffstat (limited to 'web')
| -rw-r--r-- | web/static/js/app.js | 175 | ||||
| -rw-r--r-- | web/templates/agent-context.html | 121 | ||||
| -rw-r--r-- | web/templates/agent-error.html | 28 | ||||
| -rw-r--r-- | web/templates/agent-request.html | 61 | ||||
| -rw-r--r-- | web/templates/agent-status.html | 80 |
5 files changed, 465 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(); + }); +})(); diff --git a/web/templates/agent-context.html b/web/templates/agent-context.html new file mode 100644 index 0000000..3a4778a --- /dev/null +++ b/web/templates/agent-context.html @@ -0,0 +1,121 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Agent Context - {{.AgentName}}</title> + <style> + body { font-family: system-ui, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; } + .card { background: #16213e; border-radius: 8px; padding: 24px; margin-bottom: 20px; } + h1 { color: #e94560; margin-top: 0; } + h2 { color: #0f3460; font-size: 1.2em; margin-top: 24px; } + .label { color: #888; font-size: 0.9em; margin-bottom: 4px; } + .value { font-family: monospace; background: #0f0f23; padding: 8px 12px; border-radius: 4px; word-break: break-all; margin-bottom: 16px; } + .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin-bottom: 20px; } + .summary-item { background: #0f3460; padding: 12px; border-radius: 6px; text-align: center; } + .summary-value { font-size: 1.5em; font-weight: bold; color: #e94560; } + .summary-label { font-size: 0.8em; color: #888; } + table { width: 100%; border-collapse: collapse; margin-top: 16px; } + th, td { padding: 10px; text-align: left; border-bottom: 1px solid #333; } + th { background: #0f3460; color: #fff; } + tr:hover { background: #1a1a2e; } + .source { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 0.8em; } + .source-todoist { background: #e44332; color: #fff; } + .source-trello { background: #0079bf; color: #fff; } + .source-plantoeat { background: #5cb85c; color: #fff; } + .due-overdue { color: #dc3545; } + .due-today { color: #ffc107; } + .due-future { color: #28a745; } + a { color: #4da6ff; } + </style> +</head> +<body> + <script type="application/json" id="agent-data"> +{ + "generated_at": "{{.GeneratedAt}}", + "range": { + "start": "{{.RangeStart}}", + "end": "{{.RangeEnd}}" + }, + "summary": { + "total_items": {{with .Summary}}{{.total_items}}{{else}}0{{end}}, + "overdue": {{with .Summary}}{{.overdue}}{{else}}0{{end}}, + "today": {{with .Summary}}{{.today}}{{else}}0{{end}} + }, + "timeline": [{{range $i, $item := .Timeline}}{{if $i}},{{end}} + { + "id": "{{$item.ID}}", + "source": "{{$item.Source}}", + "type": "{{$item.Type}}", + "title": "{{$item.Title}}", + "description": "{{$item.Description}}", + "due": {{if $item.Due}}"{{$item.Due.Format "2006-01-02T15:04:05Z07:00"}}"{{else}}null{{end}}, + "priority": {{$item.Priority}}, + "completable": {{$item.Completable}}, + "url": "{{$item.URL}}" + }{{end}} + ] +} + </script> + + <div class="card"> + <h1>Agent Context</h1> + <p>Timeline data for <strong>{{.AgentName}}</strong></p> + + <div class="label">Generated At</div> + <div class="value">{{.GeneratedAt}}</div> + + <div class="label">Date Range</div> + <div class="value">{{.RangeStart}} to {{.RangeEnd}}</div> + </div> + + <div class="card"> + <h2>Summary</h2> + <div class="summary"> + <div class="summary-item"> + <div class="summary-value">{{with .Summary}}{{.total_items}}{{else}}0{{end}}</div> + <div class="summary-label">Total Items</div> + </div> + <div class="summary-item"> + <div class="summary-value">{{with .Summary}}{{.overdue}}{{else}}0{{end}}</div> + <div class="summary-label">Overdue</div> + </div> + <div class="summary-item"> + <div class="summary-value">{{with .Summary}}{{.today}}{{else}}0{{end}}</div> + <div class="summary-label">Due Today</div> + </div> + </div> + </div> + + <div class="card"> + <h2>Timeline</h2> + {{if .Timeline}} + <table> + <thead> + <tr> + <th>Source</th> + <th>Title</th> + <th>Due</th> + <th>Type</th> + </tr> + </thead> + <tbody> + {{range .Timeline}} + <tr> + <td><span class="source source-{{.Source}}">{{.Source}}</span></td> + <td> + {{if .URL}}<a href="{{.URL}}" target="_blank">{{.Title}}</a>{{else}}{{.Title}}{{end}} + {{if .Description}}<br><small style="color: #888;">{{.Description}}</small>{{end}} + </td> + <td>{{if .Due}}{{.Due.Format "Jan 2, 3:04 PM"}}{{else}}-{{end}}</td> + <td>{{.Type}}</td> + </tr> + {{end}} + </tbody> + </table> + {{else}} + <p>No items in the timeline for this date range.</p> + {{end}} + </div> +</body> +</html> diff --git a/web/templates/agent-error.html b/web/templates/agent-error.html new file mode 100644 index 0000000..afb3603 --- /dev/null +++ b/web/templates/agent-error.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Error - Agent API</title> + <style> + body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; } + .card { background: #16213e; border-radius: 8px; padding: 24px; margin-bottom: 20px; border-left: 4px solid #dc3545; } + h1 { color: #dc3545; margin-top: 0; } + .status-code { font-size: 3em; color: #dc3545; font-weight: bold; } + </style> +</head> +<body> + <script type="application/json" id="agent-data"> +{ + "error": "{{.Error}}", + "status": {{.Status}} +} + </script> + + <div class="card"> + <div class="status-code">{{.Status}}</div> + <h1>Error</h1> + <p>{{.Error}}</p> + </div> +</body> +</html> diff --git a/web/templates/agent-request.html b/web/templates/agent-request.html new file mode 100644 index 0000000..fee5ca4 --- /dev/null +++ b/web/templates/agent-request.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Agent Auth Request - {{.AgentName}}</title> + <style> + body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; } + .card { background: #16213e; border-radius: 8px; padding: 24px; margin-bottom: 20px; } + h1 { color: #e94560; margin-top: 0; } + .label { color: #888; font-size: 0.9em; margin-bottom: 4px; } + .value { font-family: monospace; background: #0f0f23; padding: 8px 12px; border-radius: 4px; word-break: break-all; margin-bottom: 16px; } + .status { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 0.9em; } + .status-pending { background: #ffc107; color: #000; } + .instructions { background: #1a1a2e; border: 1px solid #333; border-radius: 4px; padding: 16px; font-size: 0.9em; } + .instructions ol { margin: 0; padding-left: 20px; } + .instructions li { margin-bottom: 8px; } + code { background: #0f0f23; padding: 2px 6px; border-radius: 3px; } + </style> +</head> +<body> + <script type="application/json" id="agent-data"> +{ + "request_token": "{{.RequestToken}}", + "status": "{{.Status}}", + "poll_url": "{{.PollURL}}", + "expires_at": "{{.ExpiresAt}}", + "agent_name": "{{.AgentName}}", + "agent_id": "{{.AgentID}}" +} + </script> + + <div class="card"> + <h1>Agent Authentication Request</h1> + <p>Agent <strong>{{.AgentName}}</strong> is requesting access to your dashboard.</p> + + <div class="label">Status</div> + <div><span class="status status-pending">{{.Status}}</span></div> + <br> + + <div class="label">Request Token</div> + <div class="value">{{.RequestToken}}</div> + + <div class="label">Poll URL</div> + <div class="value">{{.PollURL}}</div> + + <div class="label">Expires At</div> + <div class="value">{{.ExpiresAt}}</div> + </div> + + <div class="instructions"> + <strong>Next Steps:</strong> + <ol> + <li>Wait for human approval on the dashboard</li> + <li>Poll the status URL: <code>GET {{.PollURL}}</code></li> + <li>When status is "approved", extract the session token from the response</li> + <li>Use the context URL to fetch your timeline data</li> + </ol> + </div> +</body> +</html> diff --git a/web/templates/agent-status.html b/web/templates/agent-status.html new file mode 100644 index 0000000..a77bb97 --- /dev/null +++ b/web/templates/agent-status.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Agent Status - {{.AgentName}}</title> + <style> + body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; background: #1a1a2e; color: #eee; } + .card { background: #16213e; border-radius: 8px; padding: 24px; margin-bottom: 20px; } + h1 { color: #e94560; margin-top: 0; } + .label { color: #888; font-size: 0.9em; margin-bottom: 4px; } + .value { font-family: monospace; background: #0f0f23; padding: 8px 12px; border-radius: 4px; word-break: break-all; margin-bottom: 16px; } + .status { display: inline-block; padding: 4px 12px; border-radius: 12px; font-size: 0.9em; } + .status-pending { background: #ffc107; color: #000; } + .status-approved { background: #28a745; color: #fff; } + .status-denied { background: #dc3545; color: #fff; } + .status-expired { background: #6c757d; color: #fff; } + .success-box { background: #155724; border: 1px solid #28a745; border-radius: 4px; padding: 16px; margin-top: 20px; } + .instructions { background: #1a1a2e; border: 1px solid #333; border-radius: 4px; padding: 16px; font-size: 0.9em; } + code { background: #0f0f23; padding: 2px 6px; border-radius: 3px; } + </style> +</head> +<body> + <script type="application/json" id="agent-data"> +{ + "status": "{{.Status}}"{{if .SessionToken}}, + "session_token": "{{.SessionToken}}", + "context_url": "{{.ContextURL}}", + "expires_at": "{{.SessionExpiresAt}}"{{end}} +} + </script> + + <div class="card"> + <h1>Agent Status</h1> + <p>Status for agent <strong>{{.AgentName}}</strong></p> + + <div class="label">Status</div> + <div><span class="status status-{{.Status}}">{{.Status}}</span></div> + + {{if eq .Status "approved"}} + <div class="success-box"> + <strong>Access Granted!</strong> + <p>Your session has been approved. Use the credentials below to access the context.</p> + </div> + + <br> + <div class="label">Session Token</div> + <div class="value">{{.SessionToken}}</div> + + <div class="label">Context URL</div> + <div class="value">{{.ContextURL}}</div> + + {{if .SessionExpiresAt}} + <div class="label">Session Expires At</div> + <div class="value">{{.SessionExpiresAt}}</div> + {{end}} + + <div class="instructions"> + <strong>Next Step:</strong> + <p>Fetch your timeline data by navigating to: <code>GET {{.ContextURL}}</code></p> + </div> + {{else if eq .Status "pending"}} + <div class="instructions"> + <strong>Waiting for approval...</strong> + <p>Refresh this page to check the current status.</p> + </div> + {{else if eq .Status "denied"}} + <div class="instructions"> + <strong>Access Denied</strong> + <p>Your request was denied by the dashboard owner.</p> + </div> + {{else if eq .Status "expired"}} + <div class="instructions"> + <strong>Request Expired</strong> + <p>Your authentication request has expired. Please initiate a new request.</p> + </div> + {{end}} + </div> +</body> +</html> |
