Skip to main content
Glama

MCP Agent - AI Expense Tracker

by dev-muhammad
index.html18.1 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Expense Tracker Dashboard</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } :root { --primary: #6366f1; --primary-dark: #4f46e5; --success: #10b981; --danger: #ef4444; --warning: #f59e0b; --bg-main: #0f172a; --bg-card: #1e293b; --bg-hover: #334155; --text-primary: #f1f5f9; --text-secondary: #94a3b8; --border: #334155; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: var(--bg-main); color: var(--text-primary); line-height: 1.6; } .container { max-width: 1400px; margin: 0 auto; padding: 2rem; } header { margin-bottom: 3rem; } h1 { font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem; background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .subtitle { color: var(--text-secondary); font-size: 1.1rem; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-bottom: 3rem; } .stat-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 1rem; padding: 1.5rem; transition: transform 0.2s, box-shadow 0.2s; } .stat-card:hover { transform: translateY(-2px); box-shadow: 0 10px 30px rgba(99, 102, 241, 0.2); } .stat-label { font-size: 0.875rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; } .stat-value { font-size: 2rem; font-weight: 700; margin-bottom: 0.25rem; } .stat-change { font-size: 0.875rem; display: flex; align-items: center; gap: 0.25rem; } .positive { color: var(--success); } .negative { color: var(--danger); } .neutral { color: var(--text-secondary); } .section { margin-bottom: 3rem; } .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; } .section-title { font-size: 1.5rem; font-weight: 600; } .card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 1rem; overflow: hidden; } .table-container { overflow-x: auto; } table { width: 100%; border-collapse: collapse; } thead { background: var(--bg-hover); } th { padding: 1rem; text-align: left; font-weight: 600; color: var(--text-secondary); font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; } td { padding: 1rem; border-top: 1px solid var(--border); } tbody tr { transition: background 0.2s; } tbody tr:hover { background: var(--bg-hover); } .badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; } .badge-income { background: rgba(16, 185, 129, 0.1); color: var(--success); } .badge-expense { background: rgba(239, 68, 68, 0.1); color: var(--danger); } .category-item { display: flex; justify-content: space-between; align-items: center; padding: 1rem; border-bottom: 1px solid var(--border); } .category-item:last-child { border-bottom: none; } .category-info { flex: 1; } .category-name { font-weight: 600; margin-bottom: 0.25rem; text-transform: capitalize; } .category-count { font-size: 0.875rem; color: var(--text-secondary); } .category-amount { font-weight: 700; font-size: 1.25rem; } .progress-bar { height: 0.5rem; background: var(--bg-hover); border-radius: 9999px; overflow: hidden; margin-top: 0.5rem; } .progress-fill { height: 100%; background: linear-gradient(90deg, var(--primary), var(--primary-dark)); transition: width 0.3s ease; } .loading { text-align: center; padding: 3rem; color: var(--text-secondary); } .spinner { border: 3px solid var(--border); border-top-color: var(--primary); border-radius: 50%; width: 40px; height: 40px; animation: spin 0.8s linear infinite; margin: 0 auto 1rem; } @keyframes spin { to { transform: rotate(360deg); } } .empty-state { text-align: center; padding: 3rem; color: var(--text-secondary); } .empty-state-icon { font-size: 3rem; margin-bottom: 1rem; opacity: 0.3; } .amount { font-weight: 600; } .date { color: var(--text-secondary); font-size: 0.875rem; } .tabs { display: flex; gap: 0.5rem; margin-bottom: 1.5rem; border-bottom: 2px solid var(--border); } .tab { padding: 0.75rem 1.5rem; background: transparent; border: none; color: var(--text-secondary); font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s; border-bottom: 2px solid transparent; margin-bottom: -2px; } .tab:hover { color: var(--text-primary); background: var(--bg-hover); } .tab.active { color: var(--primary); border-bottom-color: var(--primary); } .tab-content { display: none; } .tab-content.active { display: block; } @media (max-width: 768px) { .container { padding: 1rem; } h1 { font-size: 2rem; } .stats-grid { grid-template-columns: 1fr; } .tabs { gap: 0.25rem; } .tab { padding: 0.5rem 1rem; font-size: 0.875rem; } table { font-size: 0.875rem; } th, td { padding: 0.75rem 0.5rem; } } </style> </head> <body> <div class="container"> <header> <h1>💰 Expense Tracker</h1> <p class="subtitle">Track your income and expenses with ease</p> </header> <div id="loading" class="loading"> <div class="spinner"></div> <p>Loading dashboard...</p> </div> <div id="dashboard" style="display: none;"> <!-- Summary Stats --> <div class="stats-grid"> <div class="stat-card"> <div class="stat-label">Total Income</div> <div class="stat-value positive" id="total-income">0.00</div> <div class="stat-change neutral"> <span id="income-count">0</span> transactions </div> </div> <div class="stat-card"> <div class="stat-label">Total Expenses</div> <div class="stat-value negative" id="total-expenses">0.00</div> <div class="stat-change neutral"> <span id="expense-count">0</span> transactions </div> </div> <div class="stat-card"> <div class="stat-label">Net Balance</div> <div class="stat-value" id="net-balance">0.00</div> <div class="stat-change neutral"> <span id="transaction-count">0</span> total transactions </div> </div> </div> <!-- Tabbed Content --> <div class="section"> <div class="tabs"> <button class="tab active" onclick="switchTab('categories')"> 📊 Categories </button> <button class="tab" onclick="switchTab('transactions')"> 🔄 Latest Transactions </button> </div> <!-- Category Summary Tab --> <div id="categories-tab" class="tab-content active"> <div class="card" id="category-summary"> <div class="empty-state"> <div class="empty-state-icon">📂</div> <p>No categories found</p> </div> </div> </div> <!-- Latest Transactions Tab --> <div id="transactions-tab" class="tab-content"> <div class="card"> <div class="table-container" id="transactions-table"> <div class="empty-state"> <div class="empty-state-icon">💸</div> <p>No transactions found</p> </div> </div> </div> </div> </div> </div> </div> <script> const API_BASE = window.location.origin; function switchTab(tabName) { // Remove active class from all tabs and tab contents document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); // Add active class to selected tab and content event.target.classList.add('active'); document.getElementById(`${tabName}-tab`).classList.add('active'); } async function fetchData(endpoint) { const response = await fetch(`${API_BASE}${endpoint}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); } function formatCurrency(amount) { return new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount); } function formatDate(dateString) { const date = new Date(dateString); return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }).format(date); } function renderSummary(summary) { document.getElementById('total-income').textContent = formatCurrency(summary.total_income); document.getElementById('total-expenses').textContent = formatCurrency(summary.total_expenses); document.getElementById('net-balance').textContent = formatCurrency(summary.net_balance); document.getElementById('income-count').textContent = summary.income_count; document.getElementById('expense-count').textContent = summary.expense_count; document.getElementById('transaction-count').textContent = summary.transaction_count; const netBalanceEl = document.getElementById('net-balance'); netBalanceEl.className = 'stat-value ' + (summary.net_balance >= 0 ? 'positive' : 'negative'); } function renderCategorySummary(categories) { const container = document.getElementById('category-summary'); if (categories.length === 0) { container.innerHTML = ` <div class="empty-state"> <div class="empty-state-icon">📂</div> <p>No categories found</p> </div> `; return; } container.innerHTML = categories.map(cat => ` <div class="category-item"> <div class="category-info"> <div class="category-name">${cat.category.replace(/_/g, ' ')}</div> <div class="category-count">${cat.transaction_count} transaction${cat.transaction_count !== 1 ? 's' : ''}</div> <div class="progress-bar"> <div class="progress-fill" style="width: ${cat.percentage}%"></div> </div> </div> <div class="category-amount">${formatCurrency(cat.total_amount)}</div> </div> `).join(''); } function renderTransactions(transactions) { const container = document.getElementById('transactions-table'); if (transactions.length === 0) { container.innerHTML = ` <div class="empty-state"> <div class="empty-state-icon">💸</div> <p>No transactions found</p> </div> `; return; } container.innerHTML = ` <table> <thead> <tr> <th>Title</th> <th>Category</th> <th>Type</th> <th>Amount</th> <th>Date</th> </tr> </thead> <tbody> ${transactions.map(txn => ` <tr> <td> <div style="font-weight: 600;">${txn.title}</div> ${txn.description ? `<div class="date">${txn.description}</div>` : ''} </td> <td> <span style="text-transform: capitalize;"> ${txn.category.replace(/_/g, ' ')} </span> </td> <td> <span class="badge badge-${txn.type}"> ${txn.type} </span> </td> <td> <span class="amount ${txn.type === 'income' ? 'positive' : 'negative'}"> ${txn.type === 'income' ? '+' : '-'}${formatCurrency(txn.amount)} </span> </td> <td> <div class="date">${formatDate(txn.date)}</div> </td> </tr> `).join('')} </tbody> </table> `; } async function loadDashboard() { try { const [summary, categories, transactions] = await Promise.all([ fetchData('/summary'), fetchData('/summary/categories'), fetchData('/transactions?limit=10') ]); renderSummary(summary); renderCategorySummary(categories); renderTransactions(transactions); document.getElementById('loading').style.display = 'none'; document.getElementById('dashboard').style.display = 'block'; } catch (error) { console.error('Error loading dashboard:', error); document.getElementById('loading').innerHTML = ` <div class="empty-state-icon">⚠️</div> <p>Error loading dashboard. Please try again later.</p> `; } } // Load dashboard on page load loadDashboard(); // Refresh dashboard every 30 seconds setInterval(loadDashboard, 30000); </script> </body> </html>

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dev-muhammad/MCPAgent'

If you have feedback or need assistance with the MCP directory API, please join our Discord server