Spaces:
Sleeping
Sleeping
Antigravity
feat: Cloud-ready release of AI Gmail Agent with premium glassmorphism telemetry dashboard and Dockerfile
e895030 | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Gmail Agent - Dashboard</title> | |
| <!-- Premium Google Fonts: Inter & Outfit --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <!-- FontAwesome for Premium Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| /* Modern CSS Custom Properties with harmonious HSL colors */ | |
| :root { | |
| --bg-base: hsl(224, 71%, 4%); | |
| --bg-canvas: hsl(224, 71%, 6%); | |
| --glass-bg: hsla(224, 71%, 10%, 0.45); | |
| --glass-border: hsla(224, 71%, 20%, 0.4); | |
| --glass-highlight: hsla(224, 71%, 30%, 0.15); | |
| --text-primary: hsl(210, 40%, 98%); | |
| --text-secondary: hsl(215, 20%, 65%); | |
| --text-muted: hsl(215, 16%, 47%); | |
| --primary: hsl(263, 90%, 62%); | |
| --primary-glow: hsla(263, 90%, 62%, 0.5); | |
| --secondary: hsl(190, 95%, 50%); | |
| --secondary-glow: hsla(190, 95%, 50%, 0.5); | |
| --priority-high: hsl(346, 84%, 61%); | |
| --priority-high-glow: hsla(346, 84%, 61%, 0.25); | |
| --priority-medium: hsl(40, 96%, 53%); | |
| --priority-medium-glow: hsla(40, 96%, 53%, 0.25); | |
| --priority-low: hsl(142, 70%, 45%); | |
| --priority-low-glow: hsla(142, 70%, 45%, 0.25); | |
| --action-sent: hsl(263, 90%, 62%); | |
| --action-draft: hsl(190, 95%, 50%); | |
| --action-skipped: hsl(215, 16%, 47%); | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| scrollbar-width: thin; | |
| scrollbar-color: var(--glass-border) transparent; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-base); | |
| background-image: | |
| radial-gradient(at 0% 0%, hsla(263, 90%, 15%, 0.3) 0px, transparent 50%), | |
| radial-gradient(at 100% 100%, hsla(190, 95%, 15%, 0.2) 0px, transparent 50%), | |
| radial-gradient(at 50% 50%, hsla(224, 71%, 4%, 1) 0%, hsla(224, 71%, 6%, 0.9) 100%); | |
| background-attachment: fixed; | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| padding: 2.5rem 1.5rem; | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-start; | |
| } | |
| /* Container Layout */ | |
| .dashboard-container { | |
| width: 100%; | |
| max-width: 1200px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2rem; | |
| } | |
| /* Breathtaking Glowing Header */ | |
| header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 1.5rem 2rem; | |
| background: var(--glass-bg); | |
| border: 1px solid var(--glass-border); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| border-radius: 16px; | |
| box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| header::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 2px; | |
| background: linear-gradient(90deg, transparent, var(--primary), var(--secondary), transparent); | |
| } | |
| .logo-section { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .logo-icon { | |
| font-size: 2rem; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| filter: drop-shadow(0 0 10px var(--primary-glow)); | |
| animation: pulse-glow 3s infinite ease-in-out; | |
| } | |
| h1 { | |
| font-family: 'Outfit', sans-serif; | |
| font-size: 1.75rem; | |
| font-weight: 700; | |
| letter-spacing: -0.02em; | |
| background: linear-gradient(to right, var(--text-primary), var(--text-secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .status-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| background: rgba(16, 185, 129, 0.1); | |
| border: 1px solid rgba(16, 185, 129, 0.2); | |
| color: hsl(142, 70%, 55%); | |
| padding: 0.5rem 1rem; | |
| border-radius: 9999px; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| box-shadow: 0 0 15px rgba(16, 185, 129, 0.1); | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| background-color: hsl(142, 70%, 50%); | |
| border-radius: 50%; | |
| position: relative; | |
| animation: blink 1.5s infinite ease-in-out; | |
| } | |
| .status-dot::after { | |
| content: ''; | |
| position: absolute; | |
| top: -4px; | |
| left: -4px; | |
| width: 16px; | |
| height: 16px; | |
| border: 2px solid hsl(142, 70%, 50%); | |
| border-radius: 50%; | |
| animation: ripple 1.5s infinite ease-in-out; | |
| } | |
| /* Stats Grid Section */ | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); | |
| gap: 1.5rem; | |
| } | |
| .stat-card { | |
| background: var(--glass-bg); | |
| border: 1px solid var(--glass-border); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 1.25rem; | |
| transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); | |
| position: relative; | |
| overflow: hidden; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); | |
| } | |
| .stat-card::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 3px; | |
| background: transparent; | |
| transition: background 0.3s ease; | |
| } | |
| .stat-card:hover { | |
| transform: translateY(-5px); | |
| border-color: var(--glass-highlight); | |
| box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25); | |
| } | |
| .stat-card.total:hover::after { background: linear-gradient(90deg, var(--primary), var(--secondary)); } | |
| .stat-card.sent:hover::after { background: var(--priority-low); } | |
| .stat-card.draft:hover::after { background: var(--secondary); } | |
| .stat-card.skipped:hover::after { background: var(--text-muted); } | |
| .stat-icon { | |
| width: 50px; | |
| height: 50px; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.5rem; | |
| background: var(--glass-highlight); | |
| border: 1px solid var(--glass-border); | |
| color: var(--text-primary); | |
| transition: all 0.3s ease; | |
| } | |
| .stat-card:hover .stat-icon { | |
| transform: scale(1.1); | |
| } | |
| .stat-card.total .stat-icon { color: var(--primary); background: hsla(263, 90%, 62%, 0.1); border-color: hsla(263, 90%, 62%, 0.2); } | |
| .stat-card.sent .stat-icon { color: var(--priority-low); background: hsla(142, 70%, 45%, 0.1); border-color: hsla(142, 70%, 45%, 0.2); } | |
| .stat-card.draft .stat-icon { color: var(--secondary); background: hsla(190, 95%, 50%, 0.1); border-color: hsla(190, 95%, 50%, 0.2); } | |
| .stat-card.skipped .stat-icon { color: var(--text-muted); background: hsla(215, 16%, 47%, 0.1); border-color: hsla(215, 16%, 47%, 0.2); } | |
| .stat-info { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.25rem; | |
| } | |
| .stat-label { | |
| font-size: 0.875rem; | |
| color: var(--text-secondary); | |
| font-weight: 500; | |
| } | |
| .stat-value { | |
| font-family: 'Outfit', sans-serif; | |
| font-size: 1.85rem; | |
| font-weight: 700; | |
| } | |
| /* Logs Dashboard Table Section */ | |
| .logs-section { | |
| background: var(--glass-bg); | |
| border: 1px solid var(--glass-border); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.25); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| } | |
| .logs-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .logs-title { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| font-family: 'Outfit', sans-serif; | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| } | |
| .logs-title i { | |
| color: var(--primary); | |
| } | |
| .refresh-btn { | |
| background: var(--glass-highlight); | |
| border: 1px solid var(--glass-border); | |
| color: var(--text-primary); | |
| padding: 0.5rem 1rem; | |
| border-radius: 8px; | |
| font-size: 0.875rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| transition: all 0.2s ease; | |
| } | |
| .refresh-btn:hover { | |
| background: var(--glass-border); | |
| border-color: var(--text-muted); | |
| } | |
| .refresh-btn:active { | |
| transform: scale(0.95); | |
| } | |
| /* Live Table Styling */ | |
| .table-wrapper { | |
| width: 100%; | |
| overflow-x: auto; | |
| border-radius: 12px; | |
| border: 1px solid var(--glass-border); | |
| } | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| text-align: left; | |
| font-size: 0.875rem; | |
| } | |
| th { | |
| background: rgba(224, 71%, 10%, 0.8); | |
| color: var(--text-secondary); | |
| font-weight: 600; | |
| padding: 1rem 1.25rem; | |
| border-bottom: 1px solid var(--glass-border); | |
| text-transform: uppercase; | |
| font-size: 0.75rem; | |
| letter-spacing: 0.05em; | |
| } | |
| td { | |
| padding: 1.15rem 1.25rem; | |
| border-bottom: 1px solid rgba(224, 71%, 20%, 0.2); | |
| color: var(--text-primary); | |
| white-space: nowrap; | |
| } | |
| tr:last-child td { | |
| border-bottom: none; | |
| } | |
| tr { | |
| transition: background 0.2s ease; | |
| } | |
| tr:hover td { | |
| background: rgba(263, 90%, 62%, 0.02); | |
| } | |
| .sender-col { | |
| font-weight: 500; | |
| max-width: 180px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .subject-col { | |
| max-width: 320px; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| color: var(--text-primary); | |
| } | |
| /* Pill Badges */ | |
| .pill { | |
| display: inline-flex; | |
| align-items: center; | |
| padding: 0.25rem 0.75rem; | |
| border-radius: 9999px; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| text-transform: capitalize; | |
| letter-spacing: 0.02em; | |
| } | |
| /* Priority Pills */ | |
| .pill.priority-high { | |
| background: rgba(244, 63, 94, 0.1); | |
| color: var(--priority-high); | |
| border: 1px solid rgba(244, 63, 94, 0.2); | |
| box-shadow: 0 0 10px var(--priority-high-glow); | |
| } | |
| .pill.priority-medium { | |
| background: rgba(245, 158, 11, 0.1); | |
| color: var(--priority-medium); | |
| border: 1px solid rgba(245, 158, 11, 0.2); | |
| box-shadow: 0 0 10px var(--priority-medium-glow); | |
| } | |
| .pill.priority-low { | |
| background: rgba(16, 185, 129, 0.1); | |
| color: var(--priority-low); | |
| border: 1px solid rgba(16, 185, 129, 0.2); | |
| box-shadow: 0 0 10px var(--priority-low-glow); | |
| } | |
| /* Action Pills */ | |
| .pill.action-sent { | |
| background: rgba(139, 92, 246, 0.15); | |
| color: var(--action-sent); | |
| border: 1px solid rgba(139, 92, 246, 0.3); | |
| } | |
| .pill.action-draft { | |
| background: rgba(6, 182, 212, 0.15); | |
| color: var(--action-draft); | |
| border: 1px solid rgba(6, 182, 212, 0.3); | |
| } | |
| .pill.action-skipped { | |
| background: rgba(100, 116, 139, 0.15); | |
| color: var(--action-skipped); | |
| border: 1px solid rgba(100, 116, 139, 0.3); | |
| } | |
| .time-col { | |
| color: var(--text-muted); | |
| font-size: 0.8rem; | |
| } | |
| .empty-state { | |
| text-align: center; | |
| padding: 3rem 1.5rem; | |
| color: var(--text-secondary); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .empty-icon { | |
| font-size: 2.5rem; | |
| color: var(--text-muted); | |
| opacity: 0.6; | |
| } | |
| /* Config Card Panel - Elegant Cloud integration guide */ | |
| .config-guide-panel { | |
| background: var(--glass-bg); | |
| border: 1px solid var(--glass-border); | |
| backdrop-filter: blur(16px); | |
| -webkit-backdrop-filter: blur(16px); | |
| border-radius: 16px; | |
| padding: 2rem; | |
| box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.25); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.25rem; | |
| } | |
| .config-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.75rem; | |
| font-family: 'Outfit', sans-serif; | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| color: var(--secondary); | |
| } | |
| .config-body { | |
| color: var(--text-secondary); | |
| font-size: 0.9rem; | |
| line-height: 1.6; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .step-list { | |
| list-style: none; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| margin-top: 0.5rem; | |
| } | |
| .step-item { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 0.75rem; | |
| } | |
| .step-num { | |
| background: var(--secondary); | |
| color: var(--bg-base); | |
| width: 22px; | |
| height: 22px; | |
| border-radius: 50%; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 700; | |
| font-size: 0.75rem; | |
| flex-shrink: 0; | |
| margin-top: 2px; | |
| } | |
| .code-container { | |
| background: rgba(0, 0, 0, 0.3); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 8px; | |
| padding: 1rem; | |
| font-family: 'Courier New', Courier, monospace; | |
| font-size: 0.825rem; | |
| overflow-x: auto; | |
| position: relative; | |
| color: var(--text-primary); | |
| } | |
| .copy-btn { | |
| position: absolute; | |
| top: 8px; | |
| right: 8px; | |
| background: var(--glass-highlight); | |
| border: 1px solid var(--glass-border); | |
| color: var(--text-secondary); | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 6px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .copy-btn:hover { | |
| color: var(--text-primary); | |
| background: var(--glass-border); | |
| } | |
| /* Beautiful Dynamic Animations */ | |
| @keyframes pulse-glow { | |
| 0%, 100% { filter: drop-shadow(0 0 5px var(--primary-glow)); } | |
| 50% { filter: drop-shadow(0 0 15px var(--primary-glow)); } | |
| } | |
| @keyframes blink { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.4; } | |
| } | |
| @keyframes ripple { | |
| 0% { transform: scale(1); opacity: 0.8; } | |
| 100% { transform: scale(2.2); opacity: 0; } | |
| } | |
| @keyframes spin { | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .fa-spin-custom { | |
| animation: spin 1s linear infinite; | |
| } | |
| /* Responsive Breakpoints */ | |
| @media (max-width: 768px) { | |
| body { | |
| padding: 1.5rem 1rem; | |
| } | |
| header { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: 1rem; | |
| } | |
| .status-badge { | |
| align-self: flex-end; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="dashboard-container"> | |
| <!-- Dashboard Header --> | |
| <header> | |
| <div class="logo-section"> | |
| <i class="fa-solid fa-wand-magic-sparkles logo-icon"></i> | |
| <div> | |
| <h1>AI Gmail Agent</h1> | |
| <p style="color: var(--text-secondary); font-size: 0.875rem;">Autonomously managing, categorizing & responding to Gmail</p> | |
| </div> | |
| </div> | |
| <div class="status-badge"> | |
| <span class="status-dot"></span> | |
| <span>Active Syncing</span> | |
| </div> | |
| </header> | |
| <!-- Dynamic Statistics Cards --> | |
| <div class="stats-grid"> | |
| <div class="stat-card total"> | |
| <div class="stat-icon"><i class="fa-solid fa-envelope-open-text"></i></div> | |
| <div class="stat-info"> | |
| <span class="stat-label">Total Processed</span> | |
| <span class="stat-value" id="stat-total">0</span> | |
| </div> | |
| </div> | |
| <div class="stat-card sent"> | |
| <div class="stat-icon"><i class="fa-solid fa-paper-plane"></i></div> | |
| <div class="stat-info"> | |
| <span class="stat-label">Auto Sent Replies</span> | |
| <span class="stat-value" id="stat-sent">0</span> | |
| </div> | |
| </div> | |
| <div class="stat-card draft"> | |
| <div class="stat-icon"><i class="fa-solid fa-file-pen"></i></div> | |
| <div class="stat-info"> | |
| <span class="stat-label">Drafts Created</span> | |
| <span class="stat-value" id="stat-drafts">0</span> | |
| </div> | |
| </div> | |
| <div class="stat-card skipped"> | |
| <div class="stat-icon"><i class="fa-solid fa-forward"></i></div> | |
| <div class="stat-info"> | |
| <span class="stat-label">Spam/Skipped</span> | |
| <span class="stat-value" id="stat-skipped">0</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Live Activity Log --> | |
| <div class="logs-section"> | |
| <div class="logs-header"> | |
| <div class="logs-title"> | |
| <i class="fa-solid fa-clock-rotate-left"></i> | |
| <h2>Live Decision Log</h2> | |
| </div> | |
| <button class="refresh-btn" id="refresh-trigger"> | |
| <i class="fa-solid fa-rotate" id="refresh-icon"></i> | |
| <span>Refresh</span> | |
| </button> | |
| </div> | |
| <div class="table-wrapper"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Sender</th> | |
| <th>Subject</th> | |
| <th>Category</th> | |
| <th>Priority</th> | |
| <th>Action Taken</th> | |
| <th>Processed Time</th> | |
| </tr> | |
| </thead> | |
| <tbody id="logs-tbody"> | |
| <tr> | |
| <td colspan="6"> | |
| <div class="empty-state"> | |
| <i class="fa-solid fa-circle-notch fa-spin empty-icon"></i> | |
| <p>Loading agent telemetry...</p> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Cloud Configuration Guide Panel --> | |
| <div class="config-guide-panel"> | |
| <div class="config-header"> | |
| <i class="fa-solid fa-cloud-arrow-up"></i> | |
| <h2>Cloud Deployment & Hugging Face Guide</h2> | |
| </div> | |
| <div class="config-body"> | |
| <p>This Gmail Agent runs entirely autonomously in the cloud. Since you are deploying to a secure, public or private Hugging Face Space, you <strong>do not</strong> need to expose your sensitive credential files in the repository. Instead, configure them as <strong>Repository Secrets</strong> in the Space settings:</p> | |
| <ul class="step-list"> | |
| <li class="step-item"> | |
| <span class="step-num">1</span> | |
| <div> | |
| <strong>GROQ_API_KEY</strong>: Generate an API key on Groq Console and add it as a secret. | |
| </div> | |
| </li> | |
| <li class="step-item"> | |
| <span class="step-num">2</span> | |
| <div> | |
| <strong>GMAIL_TOKEN_JSON</strong>: Copy the exact contents of your local <code>token.json</code> and paste it as a secret. The agent will handle session refresh token rotation automatically! | |
| </div> | |
| </li> | |
| </ul> | |
| <p style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-muted);"> | |
| <i class="fa-solid fa-circle-info"></i> Note: For security, never commit <code>token.json</code>, <code>credentials.json</code>, or <code>.env</code> files to the public Git repository. They have been automatically added to the <code>.gitignore</code> and <code>.dockerignore</code>. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Script for Dynamic Operations --> | |
| <script> | |
| const statTotal = document.getElementById('stat-total'); | |
| const statSent = document.getElementById('stat-sent'); | |
| const statDrafts = document.getElementById('stat-drafts'); | |
| const statSkipped = document.getElementById('stat-skipped'); | |
| const logsTbody = document.getElementById('logs-tbody'); | |
| const refreshBtn = document.getElementById('refresh-trigger'); | |
| const refreshIcon = document.getElementById('refresh-icon'); | |
| // Format raw date strings neatly | |
| function formatTime(isoString) { | |
| if (!isoString) return 'N/A'; | |
| const date = new Date(isoString); | |
| return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) + | |
| ' ' + date.toLocaleDateString([], { month: 'short', day: 'numeric' }); | |
| } | |
| // Fetch Stats & Logs from backend | |
| async function fetchTelemetry() { | |
| refreshIcon.classList.add('fa-spin-custom'); | |
| try { | |
| // 1. Fetch Stats | |
| const statsResponse = await fetch('/api/stats'); | |
| if (statsResponse.ok) { | |
| const stats = await statsResponse.json(); | |
| statTotal.textContent = stats.total_processed || 0; | |
| statSent.textContent = stats.auto_sent || 0; | |
| statDrafts.textContent = stats.drafts_saved || 0; | |
| statSkipped.textContent = stats.skipped || 0; | |
| } | |
| // 2. Fetch Logs | |
| const logsResponse = await fetch('/api/logs?limit=15'); | |
| if (logsResponse.ok) { | |
| const logs = await logsResponse.json(); | |
| if (logs.length === 0) { | |
| logsTbody.innerHTML = ` | |
| <tr> | |
| <td colspan="6"> | |
| <div class="empty-state"> | |
| <i class="fa-solid fa-inbox empty-icon"></i> | |
| <p>No processed emails found in the tracking database yet.</p> | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| } else { | |
| logsTbody.innerHTML = logs.map(log => { | |
| // Map Priority Pill | |
| let priorityClass = 'priority-low'; | |
| if (log.priority === 'High') priorityClass = 'priority-high'; | |
| else if (log.priority === 'Medium') priorityClass = 'priority-medium'; | |
| // Map Action Pill | |
| let actionClass = 'action-skipped'; | |
| let actionLabel = 'Skipped'; | |
| if (log.action_taken === 'auto_sent') { | |
| actionClass = 'action-sent'; | |
| actionLabel = 'Replied'; | |
| } else if (log.action_taken === 'draft_saved') { | |
| actionClass = 'action-draft'; | |
| actionLabel = 'Drafted'; | |
| } | |
| // Clean subject/sender | |
| const sender = log.sender ? log.sender.replace(/<.*>/, '').trim() : 'Unknown'; | |
| const subject = log.subject || '(No Subject)'; | |
| return ` | |
| <tr> | |
| <td class="sender-col" title="${log.sender}">${sender}</td> | |
| <td class="subject-col" title="${subject}">${subject}</td> | |
| <td><span style="font-weight: 500;">${log.category || 'Other'}</span></td> | |
| <td><span class="pill ${priorityClass}">${log.priority || 'Low'}</span></td> | |
| <td><span class="pill ${actionClass}" title="${log.notes || ''}">${actionLabel}</span></td> | |
| <td class="time-col">${formatTime(log.processed_at)}</td> | |
| </tr> | |
| `; | |
| }).join(''); | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Error loading agent telemetry:', error); | |
| logsTbody.innerHTML = ` | |
| <tr> | |
| <td colspan="6"> | |
| <div class="empty-state" style="color: var(--priority-high);"> | |
| <i class="fa-solid fa-triangle-exclamation empty-icon" style="color: var(--priority-high);"></i> | |
| <p>Failed to connect to agent API backend. Check if service is active.</p> | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| } finally { | |
| setTimeout(() => { | |
| refreshIcon.classList.remove('fa-spin-custom'); | |
| }, 400); | |
| } | |
| } | |
| // Setup manual and auto refresh loops | |
| refreshBtn.addEventListener('click', fetchTelemetry); | |
| // Initial Fetch | |
| fetchTelemetry(); | |
| // Auto Refresh every 10 seconds | |
| setInterval(fetchTelemetry, 10000); | |
| </script> | |
| </body> | |
| </html> | |